10w定时任务,如何高效触发超时

本文介绍了一种使用环形队列优化大量定时/超时任务管理的方法,相较于传统轮询扫描法或多timer触发法,该方法仅需一个timer且能批量处理超时任务,极大降低了CPU消耗。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、缘起

很多时候,业务有定时任务或者定时超时的需求,当任务量很大时,可能需要维护大量的timer,或者进行低效的扫描。

例如:58到家APP实时消息通道系统,对每个用户会维护一个APP到服务器的TCP连接,用来实时收发消息,对这个TCP连接,有这样一个需求:“如果连续30s没有请求包(例如登录,消息,keepalive包),服务端就要将这个用户的状态置为离线”。

其中,单机TCP同时在线量约在10w级别,keepalive请求包大概30s一次,吞吐量约在3000qps。

一般来说怎么实现这类需求呢?

  • “轮询扫描法”
    1)用一个Map<uid, last_packet_time>来记录每一个uid最近一次请求时间last_packet_time
    2)当某个用户uid有请求包来到,实时更新这个Map
    3)启动一个timer,当Map中不为空时,轮询扫描这个Map,看每个uid的last_packet_time是否超过30s,如果超过则进行超时处理

  • “多timer触发法”
    1)用一个Map<uid, last_packet_time>来记录每一个uid最近一次请求时间last_packet_time
    2)当某个用户uid有请求包来到,实时更新这个Map,并同时对这个uid请求包启动一个timer,30s之后触发
    3)每个uid请求包对应的timer触发后,看Map中,查看这个uid的last_packet_time是否超过30s,如果超过则进行超时处理

方案一:只启动一个timer,但需要轮询,效率较低
方案二:不需要轮询,但每个请求包要启动一个timer,比较耗资源

特别在同时在线量很大时,很容易CPU100%,如何高效维护和触发大量的定时/超时任务,是本文要讨论的问题。

二、环形队列法

废话不多说,三个重要的数据结构:
1)30s超时,就创建一个index从0到30的环形队列(本质是个数组)
2)环上每一个slot是一个Set<uid>,任务集合
3)同时还有一个Map<uid, index>,记录uid落在环上的哪个slot里

同时:

1)启动一个timer,每隔1s,在上述环形队列中移动一格,0->1->2->3…->29->30->0…
2)有一个Current Index指针来标识刚检测过的slot

当有某用户uid有请求包到达时:
1)从Map结构中,查找出这个uid存储在哪一个slot里
2)从这个slot的Set结构中,删除这个uid
3)将uid重新加入到新的slot中,具体是哪一个slot呢 => Current Index指针所指向的上一个slot,因为这个slot,会被timer在30s之后扫描到
(4)更新Map,这个uid对应slot的index值

哪些元素会被超时掉呢?
Current Index每秒种移动一个slot,这个slot对应的Set<uid>中所有uid都应该被集体超时!如果最近30s有请求包来到,一定被放到Current Index的前一个slot了,Current Index所在的slot对应Set中所有元素,都是最近30s没有请求包来到的。

所以,当没有超时时,Current Index扫到的每一个slot的Set中应该都没有元素。

优势:
(1)只需要1个timer
(2)timer每1s只需要一次触发,消耗CPU很低
(3)批量超时,Current Index扫到的slot,Set中所有元素都应该被超时掉

三、总结

这个环形队列法是一个通用的方法,Set和Map中可以是任何task,本文的uid是一个最简单的举例。

HashedWheelTimer也是类似的原理,有兴趣的同学可以百度一下这个数据结构,Netty中的一个工具类。



來源:微信公众号,架构师之路

<think>嗯,用户的问题是关于动态延迟任务的实现方法,涉及任务调度和并发编程。从用户提供的引用和系统指令来看,用户需要的是具体的技术方案和实现细节,可能是在开发分布式系统或高并发服务时遇到了任务调度的需求。 用户提供的引用材料很有价值。引用[1]详细描述了调度器的策略机制,特别是动态权重轮询和饥饿保护策略,这对实现动态延迟任务很有参考意义。引用[3]提到Java的schedule()方法,展示了单次延迟任务的实现方式。引用[4]和[5]则提供了Go语言在并发编程方面的视角,尤其是Goroutine和调度器的特性。这些材料共同指向几个关键技术点:延迟队列设计、优先级调度、冷启动优化和超时处理。 用户可能正在设计一个需要处理大量延迟任务的系统,比如订单超时取消、定时推送等场景。这类系统通常面临两个核心挑战:如何高效管理数百万计的延迟任务,以及如何保证高优先级任务不被低优先级任务阻塞。引用[1]提到的“副本预热机制”和“冷启动穿透”特别值得关注,因为在实际生产环境中,冷启动延迟往往是影响响应时间的关键因素。 在技术选型上,用户可能需要比较不同语言的解决方案。Java的ScheduledThreadPoolExecutor适合中等规模的延迟任务,但遇到海量任务时需要结合Redis或Kafka实现分布式延迟队列。Go语言通过Goroutine可以轻松创建百万级任务,但要注意用time.AfterFunc配合优先级队列实现精确调度。引用[5]强调的“worker池”模式在这里也很适用。 对于动态调整延迟的需求,引用[1]的“调度权重变更阈值”提供了重要思路——直接修改队列中任务的延迟时间可能引发性能问题,更优解是设计可重新调度的任务接口,让任务在到期前能重新计算并插入新位置。时间轮算法(如Netty的HashedWheelTimer)在这里是性能关键,它能将插入/删除操作降到O(1)复杂度。 最后想到用户可能关心的容错问题。引用[1]提到的“超时回收机制”和“调度审计日志”必须落地,否则一旦任务积压可能导致系统雪崩。建议在回答时强调这些运维层面的设计,比如为每个任务附加唯一ID以便追踪,设置内存占用阈值触发降级等。</think>以下是关于动态延迟任务实现的核心技术方案,结合任务调度与并发编程的最佳实践: ### 一、动态延迟任务的核心实现机制 1. **时间轮算法 (Time Wheel)** - 将时间划分为多个槽位(slot),每个槽位对应一个双向链表存储任务 - 时间复杂度:$O(1)$ 插入/删除任务,优于传统优先队列的 $O(\log n)$ ```java // Netty 的 HashedWheelTimer 实现示例 Timer timer = new HashedWheelTimer(10, TimeUnit.MILLISECONDS, 512); timer.newTimeout(task, 500, TimeUnit.MILLISECONDS); // 500ms后执行 ``` 2. **优先级队列 + 工作线程池** - 使用 `DelayQueue` (Java) 或 `heapq` (Python) 实现任务排序 - 工作线程循环获取到期任务执行: ```python while True: task = delay_queue.get() # 阻塞直到有到期任务 execute_task(task) ``` 3. **动态调整策略** - **权重动态计算**:根据队列负载实时调整调度权重 $W_i = \frac{P_i \times N_{\text{active}}}{\sum_{k=1}^{n} P_k}$ ($P_i$=队列优先级,$N_{\text{active}}$=活跃任务数)[^1] - **冷启动优化**:预加载高优任务副本,减少首次执行延迟[^1] ### 二、关键并发控制技术 1. **任务状态机** ```mermaid stateDiagram [*] --> PENDING : 提交 PENDING --> SCHEDULED : 入队 SCHEDULED --> RUNNING : 被线程获取 RUNNING --> COMPLETED : 执行成功 RUNNING --> FAILED : 执行异常 SCHEDULED --> CANCELLED : 主动取消 ``` 2. **饥饿防护机制** - 设置低优先级队列最大等待阈值 $T_{\text{max\_wait}}$ - 当等待时间 $t > T_{\text{max\_wait}}$ 时,临时提升调度权重: $$W_{\text{low}}' = W_{\text{low}} \times (1 + \log_2(t/T_{\text{max\_wait}}))$$[^1] 3. **超时回收策略** ```java // Java ScheduledExecutorService 示例 ScheduledFuture<?> future = executor.schedule( task, 30, TimeUnit.SECONDS); future.get(10, TimeUnit.SECONDS); // 设置执行超时控制 ``` ### 三、性能优化建议 1. **分层调度架构** ```mermaid graph LR A[API网关] --> B[调度器集群] B --> C[优先级队列组] C --> D[工作节点池] ``` - 调度周期建议:$10\text{ms} \leq T_{\text{sched}} \leq 50\text{ms}$[^1] - 单队列任务积压阈值:$Q_{\text{max}} \leq 10^4$ 2. **冷热副本分离** - 热副本:常驻内存处理实时请求 - 冷副本:按需加载处理延迟任务 - 通过 `fork()` 或容器快照实现快速副本预热[^1] 3. **审计与监控** ```python # 调度日志记录关键字段 log = { "task_id": "UUID", "assigned_worker": "node-05", "priority": "HIGH", "delay_ms": 1200, "queue": "delay-queue-3" } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值