基于最小堆的定时器的实现与使用

204 阅读9分钟

基于最小堆的定时器的实现与使用

定时器结构

主要有agent和manager两个插件,其中agent主要负责任务执行,manager负责节点的管理和任务分派。

20250303130801

集群模式下:

20250303134026

配置项

初始化配置

用于定时插件的加载和初始化

20250228090628

框架启动插件加载流程:

  1. 主线程启动,按照配置文件顺序加载各个插件库文件(so或dll)
  2. 初始化:调用各个插件库文件的OnInit接口,一般来说此时各个插件都只有一个主线程运行
  3. 启动:调用各个插件库文件的OnStart接口,一般来说此时各个插件的内部线程在此时启动

对于agent和manager两个插件,初始化函数OnInit()的作用大致相同:读取xml配置,初始化线程池,设置主备模式,初始化任务触发器

路由配置:

用于web进行定时任务操作,以及todo

<routetable functionid="2186??" pluginid="schedule_manager"/>

fsc_schedule_agent

agent插件负责任务的执行,不仅负责agent插件自身加载的定时任务,还负责执行manager下发的任务

启动OnStart

  1. 启动Cron任务触发器
  2. 启动代理线程池
  3. 追加内部任务:初始化任务、心跳任务、主备切换任务

最小堆关键字段:

20250302135616

Cron任务触发器

单线程的对象

  1. 消费完ActionQueue的所有任务,根据动作类型执行操作
  • add:将其中的任务追加到最小堆CronTaskQueue。
  • remove:将根据任务id将任务状态修改为已删除
  • clear:将所有任务状态修改为已删除,清空最小堆CronTaskQueue

如动态添加任务、在代理插件启动时,添加初始化任务

  1. 消费CronTaskQueue的任务
  • 检查时间的有效性、任务的状态是否正常
  • 任务状态正常,将其加入到fsc_manager对象任务队列

任务触发器的运行流程:

1. 初始化最小堆
2. 将ActionQueue内任务追加到最小堆
3. 取最小堆任务,加入任务队列
4. 根据任务类型更新任务的下一次执行时间
5. 将任务重新入堆
6. 检查堆顶元素key
7. 如果堆顶元素key小于等于当前时间,继续消费最小堆
8. 如果堆顶元素key大于当前时间,从第二步开始重复过程
flowchart TD
    A[初始化最小堆] --> B[将ActionQueue内任务追加到最小堆]
    B --> C[取最小堆任务, 加入线程池队列]
    C --> D[根据任务类型更新任务的下一次执行时间]
    D --> E[将任务重新入堆]
    E --> F[检查堆顶元素key]
    F --> G{堆顶元素key <= 当前时间?}
    G -- 是 --> C
    G -- 否 --> B

遇到的一个问题:

存在一些任务:

  1. 隔几秒就执行,比如时间间隔任务或者cron定时任务,他们的下一次执行时间都是取值为:当前时间+时间间隔
  2. 执行时间较长,比如调用了外部系统的函数

但是测试改大了系统时间,导致任务触发器一直在给最小堆添加任务,并提交到线程池,导致线程池的线程全部处于运行中,其他任务无法执行。

解决办法:在提交任务给线程池时,将下一次执行时间取值为当前时间。(以循环累加时间间隔的方式,直接取当前时间是不准确的)

代理线程池

主要进行任务调度,agent任务队列的任务包括agent最小堆中的,以及manager下发的,而manager不仅会下发本地定时任务,还会下发其他任务,如web端修改任务状态时218605, 如果任务存储在agent,需要通过各个agent节点更改状态信息(万一有个节点挂了,重启后任务状态不就是老的?)

所以线程池执行的任务大致的两个来源为:

  1. 任务触发器追加
  2. manager任务下发

已本地任务为例,任务执行流程为:

1. 从任务队列取任务
2. 执行任务的OnProcess
3. 判断任务状态,不可用直接将任务置为已完成
4. 判断任务执行进度
5. 若为请求任务(执行中)
6. 获取功能号和参数
7. 设置sendid,打包请求
8. 设置回调和响应包数量
9. 向proc发送请求包
10. 将任务执行状态设置为等待回调
10. 接收响应包,向任务响应队列追加响应信息
11. 将任务加入任务队列
12. 从任务队列取任务
13. 判断任务执行进度
14. 若为响应任务(回调)
15. 处理响应,将任务执行记录入库
16. 将任务置为已完成
17. 清空任务响应信息
flowchart TD
    A[从任务队列取任务] --> B[执行任务的OnProcess]
    B --> C{判断任务状态}
    C -->|不可用| D[将任务置为已完成]
    C -->|可用| E{判断任务执行进度}
    E -->|"请求任务(执行中)"| F[获取功能号和参数]
    F --> G[设置sendid,打包请求]
    G --> H[设置回调和响应包数量]
    H --> I[向proc发送请求包]
    I --> J[将任务执行状态设置为等待回调]
    J --> K[接收响应包,向任务响应队列追加响应信息]
    K --> L[执行回调,将任务加入任务队列]
    L --> A
    E -->|"响应任务(回调)"| M[处理响应,将任务执行记录入库]
    M --> N[清空任务响应信息]
    N --> D[将任务置为已完成]
  1. 执行以下接口,接口任务一部分来自内部调用,一部分来自外部调用(管理端调用)
const int FUNCTION_REFRESH = 218600;            // 心跳
const int FUNCTION_NATIVE_TASK_STATUS = 218601;     // 任务状态回调
const int FUNCTION_TASK_EXEC = 218602;               // 接收请求,执行任务
const int FUNCTION_TASK_LOG = 218603;                 // 日志记录
const int FUNCTION_NATIVE_TASK_QUERY = 218604;         // 执行节点查询接口,如查询节点信息
const int FUNCTION_CHANGE_NATIVE_TASK_STATUS = 218605;  // 修改任务状态
// ......
  1. 执行任务中的功能号

内部任务追加

初始化任务

CInitTask::OnProcess()连接manager的分支只会执行一次吧?

心跳任务要执行在初始化任务前吧,不然manager都没记录你这个节点的信息(好像也没这个判断),过期节点的判断得有一个统一过滤的接口吧,不然每写一个函数都判断是否过期?

pthread_cond_wait(&m_Cond,m_lpMutex);

本地初始化任务执行流程:

1. agent对象添加初始化任务
2. 请求manager的218604接口查询本地定时任务
3. 加载本地任务到内存
4. 将CLocalInitTask任务追加到任务队列
5. Init线程同步等待
6. CLocalInitTask任务执行OnProcess
7. 进入本地任务调用流程(初始化任务和普通任务)
8. 获取fuctionid和参数封装请求包
9. 添加回调信息
10. 设置等待响应数量
11. 向proc发送请求
12. 将m_iStep置为STEP_CALLBACK
13. 等待并接收响应,调用OncallBack
14. 将响应信息push到任务的m_RspQueue,
15. 将任务再次加入到任务队列
16. CLocalInitTask任务执行OnProcess,处理所有响应
17. 释放条件变量
18. 继续加载普通任务到最小堆
19. 更新agent任务状态
20. 定时重复发起请求
sequenceDiagram
    participant Agent
    participant Manager
    participant InitThread
    participant TaskQueue
    participant CLocalInitTask
    participant Proc
    participant MinHeap

    Agent->>Agent: 添加初始化任务
    Agent->>InitThread: 触发初始化任务
    InitThread->>Manager: 请求218604接口查询本地定时任务
    Manager-->>InitThread: 返回本地定时任务
    InitThread->>InitThread: 加载本地任务到内存
    InitThread->>TaskQueue: 追加CLocalInitTask任务
    InitThread->>InitThread: 同步等待
    TaskQueue->>CLocalInitTask: 执行OnProcess
    CLocalInitTask->>CLocalInitTask: 进入本地任务调用流程
    CLocalInitTask->>CLocalInitTask: 获取functionid和参数封装请求包
    CLocalInitTask->>CLocalInitTask: 添加回调信息
    CLocalInitTask->>CLocalInitTask: 设置等待响应数量
    CLocalInitTask->>Proc: 发送请求
    CLocalInitTask->>CLocalInitTask: 将m_iStep置为STEP_CALLBACK
    Proc->>CLocalInitTask: 执行完成,返回响应
    CLocalInitTask-->>CLocalInitTask: 接收响应,调用OnCallBack
    CLocalInitTask->>CLocalInitTask: 将响应信息push到m_RspQueue
    CLocalInitTask->>TaskQueue: 将任务再次加入任务队列
    TaskQueue->>CLocalInitTask: 执行OnProcess,处理所有响应
    CLocalInitTask->>InitThread: 释放条件变量
    InitThread->>MinHeap: 继续加载普通任务到最小堆
    MinHeap->>InitThread: 添加完成
    InitThread-->>Agent: 更新任务状态
    Agent->>InitThread: 定时重复发起初始化请求

tip:一种不知道出包的列名进行打包的写法:

int iCol = lpUnPacker->GetColCount();
for(int i = 0; i < iCol; i++ )
{
  lpAns->AddField(lpUnPacker->GetColName(i),lpUnPacker->GetColType(i),
      lpUnPacker->GetColWidth(i),lpUnPacker->GetColScale(i));
}

while(!lpUnPacker->IsEOF())
{
  // ....
}

心跳

主机agent定时向manager发送心跳,以让manager知晓哪些节点是可用的,并获取可用节点的信息,也就是心跳包信息,心跳包字段为:

node_name
task_node_name
is_master

其中task_node_name在数据库中的存储为:

20250303154237

对于manager的节点管理,也是通过各个节点agent心跳信息进行维护的,以日初定时任务为例,manager会向所有可用节点的agent下发任务, 如果任务的执行节点不可用,将不会下发

主备切换

当主机宕机后,备机成为主机,继续执行定时任务,而主机未执行完成的任务丢弃。

主机执行心跳定时任务,而备机执行主备切换定时任务:备机不断检测UFTDB的主备状态,当检测自己成为主机后,删除主备切换任务,添加心跳定时任务

主备切换检测定时任务通过CInitTask::OnProcess第一次触发后,后续的触发逻辑:

在主备正常时:

  • 主机会执行心跳的定时任务,保持插件和主机的连接
  • 备机会检测是否发生主备切换

在主备切换时:

  • 主机宕机,UFTDB将备机设置为主机
  • 备机检测自身成为主机,删除主备切换定时任务,添加心跳定时任务,这使得它后续不会再执行主备切换的定时任务了
  • 原来的主机重启成为备机后,将执行主备切换定时任务

fsc_schedule_manager

启动

  1. 启动Cron任务触发器(单线程)CTimeTrigger
  2. 启动线程池
  3. 追加内部任务:初始化任务,主备切换任务
  4. 节点管理任务

与agent的区别:

  1. 加载的本地任务类型不一样,且本地任务执行不太频繁的任务会下发到agent执行。
    • 为什么不全都放agent:怕最小堆调整起来麻烦?
    • 为什么不全都放manager: 防止频繁的跨节点,跨进程调用
  2. 接收agent的心跳,维护节点信息
  3. 负责接收远程调用,如与web端交互

manager本地定时任务执行流程 => agent本地定时任务执行流程也写一下

1.schedule_manager新增初始化任务,加载本地定时任务
2.定时任务加载到最小堆
3.最小堆任务追加到schedule_manager的线程池
4.schedule_manager执行OnProcess
5.根据taskid查询数据库获取任务信息
6.封装请求包,将任务分发到schedule_agent的线程池
7.schedule_agent封装定时任务,设置回调
8.转发定时任务请求
9. 收到响应,根据包类型执行回调
10. 将调用结果返回给schedule_manager
11. schedule_manager将定时任务执行记录写入数据库
sequenceDiagram
    participant ScheduleManager
    participant MinHeap
    participant ThreadPool_SM
    participant Database
    participant ThreadPool_SA
    participant ScheduleAgent
    participant ExternalSystem

    ScheduleManager->>ScheduleManager: 新增初始化任务,加载本地定时任务
    ScheduleManager->>MinHeap: 定时任务加载到最小堆
    MinHeap-->>ScheduleManager: 加载完成
    ScheduleManager->>ThreadPool_SM: 最小堆任务追加到线程池
    ThreadPool_SM-->>ScheduleManager: 任务追加完成
    ScheduleManager->>ScheduleManager: 执行OnProcess
    ScheduleManager->>Database: 根据taskid查询数据库获取任务信息
    Database-->>ScheduleManager: 返回任务信息
    ScheduleManager->>ThreadPool_SA: 封装请求包,分发任务到线程池
    ThreadPool_SA-->>ScheduleManager: 任务分发完成
    ThreadPool_SA->>ScheduleAgent: 封装定时任务,设置回调
    ScheduleAgent-->>ThreadPool_SA: 任务封装完成
    ScheduleAgent->>ExternalSystem: 转发定时任务请求
    ExternalSystem-->>ScheduleAgent: 收到响应
    ScheduleAgent->>ScheduleAgent: 根据包类型执行回调
    ScheduleAgent-->>ThreadPool_SA: 回调完成
    ThreadPool_SA-->>ScheduleManager: 返回调用结果
    ScheduleManager->>Database: 将定时任务执行记录写入数据库
    Database-->>ScheduleManager: 写入完成

任务回调设置

AddServiceCallback() agent插件和manager插件都有一个m_WaitMap对象【sendid,ServiceCallback】, 其中ServiceCallback和task对应,sendid存储到内存表中

当agent PostMsg插件收到一个包时,如果时请求包,根据sendid将callback对象删除,调用OnCallBack

如果有响应信息,添加响应信息,将任务追加到任务队列

任务队列消费执行OnProcess, 。。。。紧接着处理下一个响应包,将任务状态置为完成,释放条件变量

疑问

  1. 为啥agent和manager的任务要分开加载? 最后manager插件的本地函数还是给agent调用了
  2. 为啥本地定时任务要交给agent线程池去执行啊,manager线程池不也是可以执行吗,manager是不是用不着这么多线程?
  3. 什么时候要执行:ExecLocalFile,定时任务管理器有啥文件吗?
  4. SendRequest()是怎么发送请求的,往哪里发,响应怎么返回?
char szSendId[128] = {0};
snprintf(szSendId,sizeof(szSendId),"LocalScheduleTask-%d",iTaskId);
lpReq->GetItem(TAG_SENDERID)->SetString(szSendId);
CSharedPtr<IServiceCallback> lpCallback = new CBaseTaskServiceCallback(m_lpOwner,lpTask);
m_lpOwner->AddServiceCallback(szSendId,lpCallback);
SetWaitRspCount(1);
m_lpOwner->SendRequest(lpReq); 
  1. 心跳好像有点设计,agent的心跳是和谁维持心跳;manager的心跳是和各个节点维持心跳吗?

  2. 20250302163307
  3. manager.h再好好看看