IM专题:服务化架构IM系统(6)— 单线程服务框架

在前面剖析 IM 分层架构和 IM 服务架构等一系列文章中,我们知道 IM 后端整体可以抽象为三层,即入口层 Entry、业务逻辑层 Logic 和数据访问层 Das。

关于 Entry 和 Das 如何进行设计,我们分别在 “IM专题:分层架构IM系统(5)— Entry设计” 和 “IM专题:分层架构IM系统(9)— Das核心职责和逻辑设计” 中进行分析和讲解。今天我们剖析一下 Logic 中高效的单线程服务框架是如何进行设计的。

对 Entry、Logic 和 Das 三层进行抽象后,三层之间的交互方式,见下图。

IM专题:服务化架构IM系统(6)— 单线程服务框架
  • 首先,Logic 接收 Entry 的请求包;
  • 然后,Logic 请求 Das,并处理 Das 的返回;根据业务需求,Logic 与 Das 之间往往会存在多次交互;
  • 最后,Logic 将结果回复给 Entry。

上述是一个客户端发起一个请求后的基本处理流程,在这个基本流程中 Logic 的处理动作并不是连续的,而是多次等待 Das 的返回之后的分段处理动作;那么多个客户端发起多个不同的请求后,Logic 应该如何正确的处理呢?

最常规的处理方式是针对每一个客户端,Logic 启动一个独立的线程进行处理,在这个线程中完成上述图中(1)—(6)步完整的处理流程;这种方式是最容易理解,最容易落地,但也是性能最差的,只适合数量极少客户端(比如:低于50)的应用场景。

分析上图,很容易获得以下信息:

  • Logic 只与 Entry 和 Das 存在网络IO,除此之外 Logic 没有任何其他的 IO动作;
  • 对客户端的请求,Logic 存在大量的分段处理逻辑;
  • Logic 的处理逻辑主要集中在业务对象的构造、数据包序列化和反序列化、对数据结果的逻辑判断等,没有高密集的计算任务。

所以,Logic 完全可以通过一个线程来实现,满足多个客户端并发请求流程的逻辑处理;我们来对这个单线程服务框架进行逐步剖析。设计单线程服务框架模型,见下图。

IM专题:服务化架构IM系统(6)— 单线程服务框架
  • I/O 线程接收数据包,放入接收队列;这里的数据包包括 Entry 发送的请求包和 Das 返回的结果包,如上图中的步骤(1)(3)(5);
  • Worker 线程从接收队列中依次取出数据包进行处理,这里的处理动作是纯 CPU 计算,没有任何 IO 行为;
  • Worker 线程对每一个数据包处理后的结果放入发送队列;
  • I/O 线程从发送队列中依次取出结果数据包发送出去;这里的结果数据包包括发送给 Das 的请求包和返回给 Entry 的结果包,如上图中的步骤(2)(4)(6)。

单线程服务框架,并非只有一个线程,而是指真正用于业务处理的 Worker 线程只有一个,这一个 Worker 线程没有任何的 I/O 动作,用来应对多个客户端的高并发请求绰绰有余了。

上图是一个单线程服务框架模型,要对其进行落地,还需要解决很多问题。

问题一:请求上下文

首先,我们前面分析过,一个完整的请求流程是由 Worker 线程分段处理的,并不是一次连续处理完成,那这个时候就需要保存请求的上下文信息,见下图。

IM专题:服务化架构IM系统(6)— 单线程服务框架
  • 请求的上下文池信息结构是一个 Map,Map 的 key 是请求的序列化唯一标识 sid,Map 的 value 是封装的请求上下文对象 ReqContext ,该对象的成员字段信息会在后面文章中详细描述;需要注意,Map 的 key 不能是用户的唯一标识 uid,因为客户端会同时发出不同的请求;
  • Logic 接收 Entry 发送的请求后,Worker 线程会构造请求的 ReqContext 对象写入上下文池;当 Logic 向 Entry 返回结果后,Worker 线程会从上下文池中删除 ReqContext 对象。

问题二:状态机

单线程服务框架,由一个 Worker 线程处理所有客户端发出的所有的请求,由 Sid 标识一个唯一请求,并且在上下文池中存储该请求的上下文信息;那么在一次请求处理流程中,如何知晓当前 Das 的返回数据包是哪一次呢?

举个例子:在一次完整的请求处理流程中,Logic 需要访问三次 Das,那么在 Das 第一次返回时,Logic 如何知晓还需要继续访问 Das 呢?作为一个服务框架,需要给业务代码提供通用的编写业务流程的能力。这里通过状态机进行实现。

状态机本质上是一个可以枚举的变量,变量的每一个取值代表一个具体的状态,比如:状态机为 1 时,表示 Logic 第一次访问 Das 返回的结果,然后业务代码可以根据状态机的取值编写后续的处理流程。针对每一个业务请求,都会生成一个状态机变量,而该状态机变量就保存在 Sid 映射的 ReqContext 对象中,放于上下文池中。

对 Worker 线程的具体处理动作展开,设计见下图。

IM专题:服务化架构IM系统(6)— 单线程服务框架
  • 在该单线程服务框架中,定义固定的函数,由框架来调用;而固定函数的实现由业务代码来填充;
  • Entry 发送的请求包,通过 I/O 线程进入接收队列;因为是 Entry 发送的请求包,所以 Worker 线程会调用 OnRequest 函数,该函数完成对请求包的解析和状态机函数的调用;
  • 状态机模块中包含了对 Das 的读写调用,如:WriteDas 或 ReadDas;同时对状态机变量进行赋值,该状态机变量写入上下文池;对 Das 的读写请求进入发送队列,由 I/O 线程发送给 Das;
  • Das 返回的数据,通过 I/O 线程再进入接收队列,此时 Worker 线程会调用 OnPacketReceived 函数,该函数根据状态机变量的值完成对对应状态机函数的调用;上次若调用的是 WriteDas,则本次会调用对应的 OnWriteDas,意为对 Das 访问返回的事件处理;这就是状态机最核心的作用;
  • 根据业务逻辑编写业务代码,可以继续发起对 Das 的访问,如此循环,直至 EndProcess  完成整个流程处理。

问题三:超时处理

Logic 多次访问 Das ,若 Das 不能及时返回结果到 Logic ,这样的请求需要及时做超时处理,并释放占用的上下文资源,见下图。

IM专题:服务化架构IM系统(6)— 单线程服务框架
  • Worker 线程每生成一个发送到 Das 的请求包并写入到发送队列时,就在超时池中写入 Sid 元素;
  • 当 Worker 线程调用 OnPacketReceived 时,会从超时池中移除对应的 Sid 元素;
  • 启动独立的扫描线程对超时池进行定时扫描,在规定时间内没有从超时池中移走的 Sid 元素,判断为请求超时,并通过状态机进行后续的业务处理。

最后,总结文中关键:

  1. Logic 接收 Entry 请求后,与 Das 多次交互,最后返回结果给 Entry;
  2. 分析 Logic 对客户端的请求处理流程,可以实现高效的单线程服务框架;
  3. Logic 单线程服务框架需要解决三个核心问题:请求上下文、状态机和超时处理;
  4. Logic 单线程服务框架中包括三个线程:I/O 线程、Worker 线程和扫描线程;
  5. Logic 单线程服务框架独立于编程语言,可以分别通过 C++、Java、Go语言等进行实现。

作者:棕生;来自公众号“ 架构之魂”

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论