《基于单层时间轮与无锁数组操控的容器化定时器协同管理方法》---总体架构

86 阅读16分钟

接上文

# 定时管理器多线程总体架构设计

最常见的设计见上文本,设置全局共享定时管理器,管理器线程和业务线程分离。

image.png

优缺点分析

·       优点

o   定时管理器不受业务执行影响:定时任务的调度与业务逻辑分开管理,避免了业务复杂性对定时任务的干扰。即使业务负载变化,定时任务依然能够按预定时间执行,确保了任务的准确性和时效性。

o   定时漂移小:定时任务由专门的管理器控制,业务与定时任务调度独立运行,从而保证了定时任务的精度和准确性,减少了定时漂移对业务的影响。

·       缺点

o   加解锁频繁:在多线程环境下,定时任务调度器需要频繁加锁和解锁以确保线程安全,尤其在定时任务与业务并行执行时。这会导致性能下降,增加系统资源消耗。

o   执行效率不高:由于加解锁的频繁操作和资源竞争,定时任务管理器的整体效率较低,尤其在高任务量和高并发的场景下,这可能成为性能瓶颈。

o   业务需要解决同步问题:由于定时管理器与业务执行线程分离,业务需要额外的逻辑来保证定时任务与业务处理的正确顺序,从而增加了业务层的复杂度。

o   性能不高:加解锁、任务调度和资源竞争使得定时任务管理器的性能在高并发和大量任务的场景下受到影响,定时任务的调度效率可能无法满足需求。

o   可能产生死锁问题:在加锁和解锁的过程中,多个线程在相同资源上的等待可能导致死锁。这要求上层业务时刻考虑如何避免相互等待和死锁问题,进一步增加了系统的复杂性。

开源项目中的应用

虽然上述设计提供了定时任务调度的独立性和高精度,但它在高并发场景下的性能瓶颈和死锁问题使其在一些应用场景中表现不尽如人意。以下是一些开源项目,它们采用了类似的设计方案,但在某些情况下也遇到了上述缺点的挑战:

·       Nginx

o   设计概述:Nginx 使用事件驱动模型,通过 epoll 或 kqueue 等多路复用机制管理并发连接。定时任务与业务逻辑分离,在事件循环中独立处理。通过这种设计,Nginx 实现了高精度的定时任务调度,并避免了业务逻辑对定时任务的影响。

o   缺点:与传统多线程调度不同,Nginx 需要频繁加锁以确保线程安全,尤其在需要管理大量定时器时,可能导致性能瓶颈,特别是在高并发场景下。

·       libuv (Node.js)

o   设计概述:libuv 是 Node.js 的底层库,提供了跨平台的事件循环和 I/O 多路复用机制,定时任务的调度和执行独立于业务线程。通过这种方式,定时任务能够在不影响业务逻辑的情况下高效执行。

o   缺点:由于 libuv 依赖多线程和锁机制,在某些高并发场景下可能出现性能瓶颈,尤其在任务调度密集的情况下,可能导致响应延迟。

·       Quartz Scheduler

o   设计概述:Quartz 是一个功能强大的作业调度库,支持定时任务的精确调度。它通过全局调度器和线程池来管理定时任务,确保定时任务独立于主业务线程运行,从而避免业务负载对定时任务的影响。

o   缺点:在频繁的定时任务调度中,Quartz 可能会面临加解锁频繁和资源竞争问题,导致系统性能下降,尤其在高并发情况下,可能会引发复杂的死锁问题。

应用场景和解决方案

在分布式协同的业务容器中,定时任务主要用于设置等待应答的超时定时器,通常周期较短,精度要求不高。因此,上述设计中提到的高精度定时任务管理对于这些场景的意义相对较小。然而,由于性能瓶颈和业务处理的复杂性,这种设计仍然难以解决高并发、大规模任务管理中的问题。为了解决这些问题,需要考虑优化定时管理器的调度效率,减少加解锁频繁带来的性能损耗,并避免可能的死锁问题。

改进方案1:

通过采用合适的任务调度算法和资源管理策略,可以优化这些开源项目的定时管理设计,在大规模分布式环境下提升系统的性能和可扩展性。

image.png

改进方案 1

在大规模分布式环境下,通过选用适配的任务调度算法与资源管理策略,能够对这些开源项目的定时管理设计进行优化,从而显著提升系统性能与可扩展性。为攻克频繁加解锁引发的性能难题,以及有效应对高并发环境下可能出现的业务处理困境和死锁问题,众多开源项目在设计时提出了针对性的改进举措。

为规避这些问题,开源项目将定时任务与业务逻辑分离,使其在独立的线程或进程中进行调度。借助这种设计,定时任务不再与业务处理处于同一线程,有效避免了频繁的锁操作和死锁状况。同时,定时任务管理与业务逻辑的隔离,对系统性能优化、定时任务精度和可靠性提升均大有裨益。以下是一些开源项目中常见的解决方案:

1.     独立调度器与线程池分离:诸如 Akka、Quartz Scheduler 等众多开源项目,采用独立的调度器负责定时任务管理,将调度器与业务处理逻辑区分开来。定时任务由独立的定时器调度模块管控,而业务处理则由业务线程或工作线程池承担。如此一来,定时任务与业务逻辑分别在不同的线程或进程中运行,确保两者相互隔离,避免了因频繁加解锁操作致使的性能下滑。

2. 消息队列和任务队列机制: 定时任务与业务任务借助消息队列或任务队列实现解耦。在此设计模式下,定时任务通过独立的消息队列进行调度,被发送至特定的工作队列,业务逻辑则由消费者对这些任务进行处理。通过这种方式,定时任务与业务任务的执行互不阻塞,进而提高了系统的处理能力与响应速度。****

  1. 线程池与资源隔离:为防止死锁和资源竞争,许多开源项目运用线程池与资源隔离的方式,确保定时任务与业务逻辑独立执行。以 Quartz Scheduler 为例,它运用独立的线程池执行定时任务,保障定时任务的调度不会干扰业务逻辑的处理。每个任务在独立线程中执行,有效避免了资源争用和死锁的发生。
  1. 定时器任务与业务任务调度的智能分配:在部分高级任务调度系统中,定时任务与业务任务的调度会依据优先级、资源使用状况以及系统负载等因素进行智能分配。这不仅能够减少定时任务的漂移现象,还能更合理地管理系统资源,提升整体处理效率。

通过这些设计改进,开源项目成功化解了传统定时任务管理中频繁加解锁、业务处理复杂以及死锁等问题带来的挑战。定时任务与业务处理逻辑的分离,让系统在高并发和大规模任务管理场景下,能够维持高效、稳定且可扩展的运行状态。定时任务的精准调度与业务逻辑的高效处理相互促进,进一步增强了系统的性能和可维护性。

然而,上述设计也存在关键问题。在大量定时任务的场景中,业务线程的消息队列里会涌入众多定时任务消息,占用大量业务处理时间,导致业务处理延迟显著增加,定时器漂移问题严重,这在实时分布式系统中尤为突出。消息队列操作涉及加解锁出入队列,以及等待、通知 PV 操作,属于高负荷操作,整体性能表现欠佳。

image.png

改进方案2

定时管理器嵌入业务线程

设计思路

将定时管理器直接融入业务线程内部,外部仅需借助 tick 消息队列来触发定时器。这意味着定时任务的激励操作与回调执行均在同一业务线程中完成,实现了定时管理与业务线程的深度融合,避免了传统模式下多线程协作带来的复杂交互问题。

工作流程

1.     深度参与业务执行:定时管理器作为业务线程的有机组成部分,全程参与业务任务的执行过程,与业务逻辑紧密协同。

  1. 外部消息触发:外部对定时任务的激励,通过 tick 消息发送至业务线程,以此作为定时任务启动的信号。
  1. 消息处理与回调启动:业务线程在接收到 tick 消息后,立即对其进行处理,并启动相应的定时任务回调,完成定时任务的执行。

优点

·       紧密结合,降低开销:任务调度与业务处理紧密相连,减少了独立消息队列管理所产生的额外开销,包括内存占用、线程间通信开销等,提高了系统整体的运行效率。

·       设计简洁,降低复杂度:任务调度与业务逻辑在同一上下文环境中执行,无需额外的复杂同步机制和线程间协调操作,极大地降低了系统设计与实现的复杂性,同时有效规避了大量定时任务涌入消息队列引发的业务处理延迟、定时器漂移等问题 。

缺点

在实时场景中,常常需要精确到毫秒级别的定时器。即便上层业务中定时任务数量极少,定时器激励源每秒仍需向业务线程发送 1000 个 tick 消息,这使得消息队列的处理负担较重,由此产生的开销不容忽视,可能会对系统性能造成一定影响。

案例分析

·       Node.js + libuv:Node.js 的定时任务调度依赖 libuv 库,该库提供了事件循环机制,通过 setTimeout 和 setInterval 函数来管理定时任务。这些定时任务均在主事件循环中执行,主线程接收 tick 消息,并依据事件循环机制触发定时任务。虽然 Node.js 采用的是事件驱动模型,但定时任务的触发与业务逻辑处理均嵌入在同一线程中,tick 消息的触发机制使定时任务调度与业务逻辑紧密耦合,充分体现了定时管理器嵌入业务线程的设计理念。

·       Go (Golang) :Go 语言利用 goroutines 实现并发调度,定时任务一般通过 goroutines 执行,常见的 time.Tick 函数通过触发 tick 来启动定时任务。在 Go 语言中,定时任务管理由 goroutine 和调度器共同完成,任务调度与业务执行通常在同一个 goroutine 内进行,tick 消息负责触发定时任务的执行,同样符合定时管理器嵌入业务线程的设计思想。

改进方案 3:业务容器设计概述

image.png

1. 业务容器功能设计

·       网络消息处理:利用 epoll(Linux 系统)或 WaitForMultipleObjectsEx(Windows 系统)对网络连接的读写事件进行监听,实时捕捉网络数据的传输动态。

·       消息队列处理:借助 eventfd(Linux 系统)或 WSAEVENT(Windows 系统)监听消息队列的读写事件,摒弃传统的条件变量 + 锁机制,降低线程同步开销,提升消息处理效率。

·       定时器管理:通过 timerfd_create(Linux 系统)或 CreateWaitableTimer(Windows 系统)创建句柄,对定时器的到期事件进行监听,一旦定时器到期,即刻触发定时任务或定时器回调。

采用 epoll/iocp 来统一事件的监听和分发,有效规避线程间的竞争,显著提升事件处理效率,保障系统在高并发场景下的稳定运行。

2. 容器底层设计模块

·       epoll/iocp 事件循环:epoll(Linux)和 iocp(Windows)作为高效的 I/O 多路复用机制,特别适用于处理大量并发 I/O 事件。一旦事件发生,容器会依据不同的事件类型,调用相应的处理逻辑。

o   网络消息:将网络连接的 socket 添加到 epoll/iocp 中,对其可读、可写、异常等事件进行监听,并通过回调函数实现网络数据的高效收发。

o   消息队列消息:使用 eventfd/WSAEVENT 进行消息队列的消息通知,这是 Linux/Windows 提供的轻量级事件通知机制,可用于线程间通信。将 eventfd/WSAEVENT 事件句柄注册到 epoll/iocp 中,实现无锁的消息队列操作,提升并发性能。

o   定时器消息:timerfd(Linux)/handle(Windows)是一种特殊的文件描述符,代表一个定时器,可设定超时事件。将 timerfd/handle 句柄加入到 epoll/IOCP 中,当定时器超时时,触发定时回调,执行定时任务。

·       网络消息处理:在网络消息处理过程中,通常将每个连接的 socket 或 epoll/iocp 实例化为独立的事件句柄,并注册到 epoll 中,监听数据的读写事件。

o   连接管理:epoll/iocp 能够在一个线程内管理成千上万的网络连接。每当有新的连接请求,epoll/iocp 会及时通知,以便进行相应处理。

o   数据接收与发送:当连接上的数据可读时,通过回调函数处理接收到的数据;当连接可写时,同样通过回调函数发送数据。

o   实现关键点:采用非阻塞模式处理网络连接,提升网络 I/O 的效率;优化数据的处理和发送流程,避免每次事件发生时进行阻塞调用,减少线程等待时间。

·       消息队列的 eventfd/handle 实现:Eventfd/handle 是用于线程间通知的机制,非常适合实现生产者消费者模型。

o   Eventfd/handle 用于消息队列的 PV 操作:将 eventfd/handle 用作消息队列的信号通知机制,当有新的消息到达时,生产者线程将信号写入 eventfd/handle;消费者线程则读取 eventfd/handle 的值来判断是否有新消息,实现高效的消息传递。

o   集成到 epoll/iocp 中:将 eventfd/handle 的文件描述符添加到 epoll/iocp 中,当消息队列有新消息时,epoll/iocp 会触发回调函数,及时处理消息。

o   实现关键点:利用 eventfd/iocp 的 read 和 write 操作来模拟消息队列的信号量操作(P 和 V 操作),简化消息队列的实现;通过 epoll/iocp 统一管理消息队列的事件,避免传统锁机制带来的性能问题。

·       定时器管理与 timerfd/handle:Timerfd/handle 提供基于时间触发的事件,特别适合定时任务的管理。

o   定时器 tick:使用 timerfd/handle 设置定时器,监听定时器到期事件。当定时器触发时,epoll/handle 会通知定时器超时,从而执行相应的定时任务。

o   定时任务管理:设计一个定时器管理器,集中管理所有定时任务,并在定时器超时后执行相应的回调,确保定时任务的有序执行。

o   实现关键点:利用 timerfd/handle 的超时事件定期执行任务,实现精确的时间控制;通过回调机制实现定时任务的执行,如定期清理资源或周期性检查系统状态。

·       统一事件调度:所有不同类型的事件都通过一个统一的事件调度机制来处理。epoll 提供了高效的 I/O 事件处理方式,将网络事件、消息队列事件和定时器事件统一交给 epoll 来管理,形成高效的事件循环,确保系统对各类事件的及时响应。

3. 容器的运行与管理

将上述设计进一步抽象成一个业务容器模块,该模块管理着所有注册的事件,处理各种类型的消息。业务容器的运行流程如下:

1.     初始化容器:创建 epoll 实例,为后续的事件监听和处理做好准备。

  1. 添加事件源:将网络连接、消息队列 eventfd 和定时器 timerfd 添加到 epoll 中,使其能够监听这些事件源的变化。

3.     启动事件循环:通过 epoll_wait 等待事件发生,阻塞线程,直到有事件触发。

  1. 事件处理:事件触发时,根据事件类型调用不同的回调函数,进行相应的处理,确保事件得到及时、准确的处理。
  1. 返回等待:处理完事件后,回到 epoll_wait 等待下一个事件,持续循环,保证系统的持续运行。

4. 设计的优缺点

·       优点

o   高效性:epoll 提供了高效的 I/O 事件监听能力,能够支持大量并发连接,满足高并发场景下的业务需求。

o   无锁设计:通过 eventfd 和 timerfd,避免了传统的锁机制,减少了线程同步开销,提高了并发性能。

o   模块化设计:网络消息、消息队列和定时器消息的处理是解耦的,各个模块功能独立,易于扩展和维护,方便后续的功能升级和优化。

·       缺点:适用实时系统业务,上层业务如若响应以业务特别耗时,需要上层业务自行创建线程做异步操作,增加业务开发复杂度。

 

附录:个人软件领域从业超过30年,华为软件公司架构部从业经历,退休后,热衷技术研究与分享。感兴趣同事可加我微信交流:18918000629