多任务定时调度的冲突解决:错峰执行方案
定时任务在量化系统里是标配。但任务多了以后,冲突就来了——不是逻辑冲突,是资源争抢。今天聊聊我们系统里一个实际踩过的坑,以及怎么用错峰解决。
冲突场景分析
我们的系统里有两个核心定时任务:
- MA信号任务:基于均线策略生成交易信号,每小时整点执行(如
09:00, 10:00, 11:00...) - WL更新任务:更新股票池的WL数据(权重列表),原本也设定在整点执行
问题很直观:两个任务都在整点触发。当 09:00 到来时,MA信号要拉行情数据、计算均线、生成信号;WL更新也要拉数据、计算权重、写回数据库。两者同时启动,CPU 和 I/O 争抢严重,导致:
- MA信号计算延迟,信号生成时间从秒级拉长到分钟级
- WL更新任务偶发超时,数据库连接池被打满
- 日志里频繁出现
TimeoutError和ConnectionPoolError
我们最初的想法是"机器性能够好,扛得住"。但实际观察下来,两个任务叠加时的资源峰值远高于单个任务,磁盘 I/O 更是瓶颈。
错峰策略设计
解决方案不复杂:错峰。把 WL 更新任务从整点挪到 04:30 执行。
选择 04:30 有几个考虑:
- 凌晨 4:30 不是交易时段,没有行情数据更新压力
- 与 MA 信号的整点执行完全错开,不存在时间重叠
- 凌晨系统负载低,WL 更新即使耗时较长也不影响其他任务
当然,这个时间点不是固定的。如果你有其他凌晨任务,可以根据实际负载情况调整。核心原则是:让高资源消耗的任务彼此避开。
实现方案
我们使用 APScheduler 来管理定时任务。改造前的代码大致如下:
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
def ma_signal_job():
"""MA信号任务 - 每小时整点执行"""
print(f"[MA Signal] {datetime.now()} - 开始生成信号")
# 拉取行情数据
data = fetch_market_data()
# 计算均线
signals = calculate_ma_signal(data)
# 写入信号库
save_signals(signals)
print(f"[MA Signal] {datetime.now()} - 信号生成完成")
def wl_update_job():
"""WL更新任务 - 原本也在整点执行"""
print(f"[WL Update] {datetime.now()} - 开始更新权重")
# 拉取股票池数据
pool = fetch_stock_pool()
# 计算权重
weights = calculate_weights(pool)
# 更新数据库
update_weights(weights)
print(f"[WL Update] {datetime.now()} - 权重更新完成")
scheduler = BlockingScheduler()
# 两个任务都在整点执行
scheduler.add_job(
ma_signal_job,
CronTrigger(minute=0), # 每小时整点
id='ma_signal',
max_instances=1,
coalesce=True
)
scheduler.add_job(
wl_update_job,
CronTrigger(minute=0), # 同样在整点执行
id='wl_update',
max_instances=1,
coalesce=True
)
scheduler.start()
改造后,WL 任务的时间改为 04:30:
# WL更新任务错峰至04:30执行
scheduler.add_job(
wl_update_job,
CronTrigger(hour=4, minute=30), # 每日04:30执行
id='wl_update',
max_instances=1,
coalesce=True
)
就这么一行改动,问题解决了。
任务调度时间表
改造后的任务调度时间表如下:
| 任务名称 | 执行频率 | 执行时间 | 资源消耗 |
|---|---|---|---|
| MA信号 | 每小时 | 整点(如 09:00, 10:00) | 高(行情+计算) |
| WL更新 | 每日 | 04:30 | 中高(全量权重) |
从时间分布看,两个高消耗任务完全错开,不存在同时执行的情况。
验证方法
改完之后不能光看代码,要验证实际效果。我们做了三件事:
1. 日志时间戳分析
在任务入口和出口分别打印时间戳,统计任务实际执行时长:
import time
from datetime import datetime
def ma_signal_job():
start = time.time()
print(f"[MA Signal] {datetime.now()} - 开始")
# ... 任务逻辑 ...
elapsed = time.time() - start
print(f"[MA Signal] {datetime.now()} - 完成,耗时 {elapsed:.2f}s")
对比改造前后 MA 信号任务的耗时:
改造前: [MA Signal] 09:00:00 - 开始
[MA Signal] 09:01:23 - 完成,耗时 83.45s
[WL Update] 09:00:01 - 开始
[WL Update] 09:00:47 - 完成,耗时 46.12s
改造后: [MA Signal] 09:00:00 - 开始
[MA Signal] 09:00:08 - 完成,耗时 8.12s
[WL Update] 04:30:00 - 开始
[WL Update] 04:30:35 - 完成,耗时 35.20s
MA 信号从 83 秒降到 8 秒,效果立竿见影。
2. 资源监控
用 psutil 记录任务执行期间的 CPU 和内存使用:
import psutil
import threading
class ResourceMonitor:
def __init__(self):
self.records = []
self._stop = threading.Event()
def start(self):
self._stop.clear()
self.thread = threading.Thread(target=self._monitor)
self.thread.start()
def stop(self):
self._stop.set()
self.thread.join()
def _monitor(self):
while not self._stop.is_set():
cpu = psutil.cpu_percent(interval=1)
mem = psutil.virtual_memory().percent
self.records.append((datetime.now(), cpu, mem))
time.sleep(1)
改造前,09:00 时刻的 CPU 峰值经常到 90%+,改造后峰值降到 40% 以下。
3. 任务执行成功率
统计连续 7 天的任务执行情况:
from collections import defaultdict
# 模拟统计逻辑
stats = defaultdict(lambda: {'total': 0, 'failed': 0})
def track_job(job_name, func):
def wrapper(*args, **kwargs):
stats[job_name]['total'] += 1
try:
return func(*args, **kwargs)
except Exception as e:
stats[job_name]['failed'] += 1
print(f"[ERROR] {job_name}: {e}")
return wrapper
# 7天后输出统计
for job_name, s in stats.items():
success_rate = (s['total'] - s['failed']) / s['total'] * 100
print(f"{job_name}: 成功率 {success_rate:.1f}%")
改造前 WL 更新任务成功率约 95%(有超时失败),改造后 100%。
扩展:多任务错峰的通用方案
如果你的系统不止两个任务,可以用更灵活的方式管理。比如用配置文件定义任务时间:
# config.yaml
tasks:
ma_signal:
cron: "0 * * * *" # 每小时整点
enabled: true
wl_update:
cron: "30 4 * * *" # 每日04:30
enabled: true
data_backup:
cron: "0 3 * * *" # 每日03:00
enabled: true
然后用 Python 加载配置并动态注册任务:
import yaml
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
def load_tasks_from_config():
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
scheduler = BackgroundScheduler()
task_mapping = {
'ma_signal': ma_signal_job,
'wl_update': wl_update_job,
'data_backup': data_backup_job
}
for task_name, task_config in config['tasks'].items():
if not task_config['enabled']:
continue
# 将 cron 表达式转换为 CronTrigger
minute, hour, day, month, day_of_week = task_config['cron'].split()
scheduler.add_job(
task_mapping[task_name],
CronTrigger(
minute=minute,
hour=hour,
day=day,
month=month,
day_of_week=day_of_week
),
id=task_name,
max_instances=1,
coalesce=True
)
return scheduler
这样改任务时间只需要改配置文件,不用动代码。
几点经验
- 先分析再动手:改之前先确认冲突确实存在,用日志和监控数据说话
- 错峰是简单有效的方案:相比加锁、加队列,错峰更直接,几乎没有额外复杂度
- 时间点选择要合理:避开交易时段,避开其他任务高峰
- 验证必须量化:用耗时、CPU、成功率这些指标对比前后效果
定时任务冲突是量化系统里很常见的问题。错峰执行不是唯一方案,但往往是最省事的。如果你的系统任务不多,优先考虑错峰;如果任务很多,再考虑任务队列、分布式调度这些更重的方案。
更多内容请关注本站,后续会继续分享量化系统实战中的工程问题。