
本文介绍一种基于贪心轮询的python调度策略,将yaml中定义的多个脚本任务按指定次数均匀分布在60秒内执行,确保同类任务间隔尽可能均衡,且不依赖固定顺序或预展开列表。
本文介绍一种基于贪心轮询的python调度策略,将yaml中定义的多个脚本任务按指定次数均匀分布在60秒内执行,确保同类任务间隔尽可能均衡,且不依赖固定顺序或预展开列表。
在自动化运维或定时任务编排场景中,简单地将任务按频次“展开成列表再顺序执行”(如 ['test0.sh']×3 + ['test1.sh']×2)会导致任务堆积、资源争用和时间分布失衡。例如,前3秒连续运行 test0.sh,后续才执行 test1.sh,违背了“均匀散布”的核心需求。
正确的思路是:按需轮询尚未耗尽配额的任务,每次选取一个待执行次数最多的剩余任务(平权时按定义顺序),从而天然实现最大间隔均衡。这本质上是一种贪心调度(Greedy Scheduling),可类比于操作系统中的“最短剩余时间优先”思想,但目标是最大化同类任务的时间间距。
以下是完整、健壮的实现方案:
import subprocess
import time
import yaml
# 1. 加载配置
with open("tasks.yaml") as f:
config = yaml.safe_load(f)
tasks = config.get("every_minute", [])
# 2. 构建任务-剩余次数映射(保持原始顺序用于平权决策)
task_queue = []
task_counts = {}
for task in tasks:
path = task["path"]
times = task["times_per_min"]
task_counts[path] = times
# 记录原始顺序,便于后续稳定轮询(避免字典无序导致行为不可控)
task_queue.append(path)
# 3. 贪心轮询生成均匀序列
scheduled_tasks = []
remaining = task_counts.copy()
while sum(remaining.values()) > 0:
# 按原始定义顺序遍历,优先选择当前剩余次数 > 0 的第一个任务
# (等效于“轮询”,且保证相同配置下结果确定)
selected = None
for path in task_queue:
if remaining[path] > 0:
selected = path
break
if selected is None:
break # 理论上不会触发
scheduled_tasks.append(selected)
remaining[selected] -= 1
# 4. 执行调度(每60秒均分间隔)
if scheduled_tasks:
interval = 60.0 / len(scheduled_tasks)
processes = [] # 显式声明,避免未定义错误
for task_path in scheduled_tasks:
try:
proc = subprocess.Popen([task_path])
processes.append(proc)
except FileNotFoundError:
print(f"❌ Error: Script '{task_path}' not found.")
except Exception as e:
print(f"⚠️ Error running '{task_path}': {e}")
time.sleep(interval)
# 可选:等待所有子进程结束(根据实际需求决定)
# for p in processes:
# p.wait()
else:
print("ℹ️ No tasks scheduled.")
✅ 关键优势说明:
-
真正均匀:对
test0.sh×3,test1.sh×2,输出为['test0.sh', 'test1.sh', 'test0.sh', 'test1.sh', 'test0.sh'],相邻test0.sh间隔为 2 或 3 个槽位,远优于全堆叠方案; -
可扩展性强:支持任意数量任务及任意正整数
times_per_min(包括1,10,100); - 确定性行为:严格按 YAML 中任务定义顺序轮询,相同输入必得相同调度序列;
- 轻量高效:时间复杂度 O(N×M),其中 N 为任务数、M 为总执行次数,内存仅存路径字符串。
⚠️ 注意事项:
- 若某脚本执行时间显著长于
interval(如 >12 秒),将导致后续任务延迟甚至并发挤压。建议在脚本内做超时控制,或改用异步非阻塞执行(如asyncio.subprocess); -
subprocess.Popen启动后即返回,不等待完成。若需串行执行(一个结束再启下一个),应替换为subprocess.run(); - 生产环境请增加日志记录、错误重试、资源监控等机制,本示例聚焦调度逻辑本身。
通过该方法,你获得的不再是一维线性队列,而是一个为时间维度优化的动态调度平面——让每个任务在60秒的“时间环”上找到它最公平的落点。










