1. 问题现象与复现过程
第一次遇到DolphinScheduler的服务器雪崩问题时,我正在深夜处理一个紧急告警。监控大屏突然显示CPU使用率飙升至100%,紧接着内存耗尽,整个调度系统彻底瘫痪。经过排查,发现问题出在Master节点重启后触发的任务补偿机制上。
具体复现步骤是这样的:假设我们有一个简单的Shell任务,内容如下:
current_timestamp() { date +"%Y-%m-%d %H:%M:%S" } TIMESTAMP=$(current_timestamp) echo $TIMESTAMP sleep 60当我们将这个工作流设置为每10秒触发一次的并行调度时,正常情况下系统会稳定运行。但如果我们突然kill掉Master进程:
jps | grep MasterServer | awk '{print $1}' | xargs kill -9等待一段时间后重启整个集群:
bin/stop-all.sh bin/start-all.sh这时灾难就发生了——系统会瞬间补偿执行所有积压的调度任务。如果这些任务都是计算密集型操作,服务器资源会在几秒内被耗尽,就像我遇到的情况一样。
2. 核心机制原理解析
2.1 Quartz的Misfire机制
问题的根源在于DolphinScheduler与Quartz集成的调度补偿机制。Quartz作为成熟的调度框架,设计了一套完善的Misfire处理策略。当满足以下条件时,任务会被标记为Misfire状态:
- 任务到达触发时间时未被执行
- 延迟时间超过配置的阈值(默认60秒)
在DolphinScheduler中,Quartz的触发器配置是这样的:
CronTrigger cronTrigger = newTrigger() .withIdentity(triggerKey) .startAt(startDate) .endAt(endDate) .withSchedule( cronSchedule(cronExpression) .withMisfireHandlingInstructionIgnoreMisfires() .inTimeZone(DateUtils.getTimezone(timezoneId))) .forJob(jobDetail).build();关键点在于.withMisfireHandlingInstructionIgnoreMisfires()这个配置,它对应的策略代码是:
public CronScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() { this.misfireInstruction = -1; // MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY return this; }这个策略意味着:当Master节点恢复后,所有错过的触发事件都会被立即补偿执行,没有任何限制。
2.2 DolphinScheduler的任务调度流程
整个调度生命周期可以分为几个关键阶段:
调度触发阶段:
- Web界面创建调度后,数据写入t_ds_schedules表
- Quartz创建对应的触发器,记录在QRTZ_CRON_TRIGGERS表
- ProcessScheduleTask定期将待调度任务写入t_ds_command表
任务执行阶段:
- MasterServer从t_ds_command表获取任务
- 生成ProcessInstance写入t_ds_process_instance表
- WorkerServer执行具体任务并反馈状态
当Master节点宕机时,这个流程会在两个地方产生积压:
- Quartz侧会积累未触发的调度事件
- DolphinScheduler侧会积累未处理的command记录
3. 问题定位与源码分析
3.1 关键线程分析
通过分析MasterServer的启动流程,我们发现以下几个关键线程:
public void run() throws SchedulerException { this.masterRPCServer.start(); this.taskPluginManager.loadPlugin(); this.masterSlotManager.start(); this.masterRegistryClient.start(); this.masterSchedulerBootstrap.start(); this.eventExecuteService.start(); this.failoverExecuteThread.start(); // 重点关注 this.schedulerApi.start(); this.taskGroupCoordinator.start(); // ... 监控指标注册 }其中failoverExecuteThread负责故障恢复,但实际它只处理未完成的任务实例,并不涉及调度补偿。真正的补偿逻辑藏在Quartz的触发器配置中。
3.2 补偿触发路径
通过代码回溯,我们找到核心触发链路:
ProcessScheduleTask.executeInternal()方法从Quartz获取调度时间- 判断是否为Misfire状态(延迟超过阈值)
- 根据配置的策略(-1)执行补偿动作
- 将补偿任务写入t_ds_command表
- MasterServer消费这些command并创建大量ProcessInstance
这个设计在低频任务场景下没有问题,但在高频调度(如10秒一次)且宕机时间较长(如30分钟)时,会产生180个待补偿任务(30×60/10),瞬间爆发导致系统过载。
4. 解决方案与实施
4.1 策略调整方案
经过分析,我们有以下几种可能的解决方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 修改为串行执行 | 简单直接 | 丧失并行处理能力 |
| 增加Master节点 | 提高可用性 | 无法避免长时间宕机后的补偿 |
| 调整Misfire策略 | 根治问题 | 需要修改源码 |
最终我们选择修改Quartz的Misfire策略,将配置改为:
.withSchedule( cronSchedule(cronExpression) .withMisfireHandlingInstructionDoNothing() // 修改为2 .inTimeZone(DateUtils.getTimezone(timezoneId)))对应的策略常量:
int MISFIRE_INSTRUCTION_DO_NOTHING = 2; // 对于CronTrigger,忽略所有错过的触发4.2 实施步骤
具体实施需要重新编译DolphinScheduler:
- 修改
dolphinscheduler-scheduler-quartz模块的触发器配置代码 - 执行模块编译:
mvn spotless:apply clean package -Dmaven.test.skip=true -Prelease- 替换生产环境jar包:
cp dolphinscheduler-scheduler-quartz-3.2.1.jar \ /opt/dolphinscheduler/master-server/libs/- 滚动重启集群服务
4.3 验证效果
修改后我们进行了验证:
- 创建高频调度任务(10秒间隔)
- 模拟Master宕机(kill -9)
- 等待30分钟后重启服务
- 观察系统行为
新的表现:
- 错过执行窗口的任务不会被补偿
- 系统资源保持平稳
- 新的调度任务正常执行
5. 生产环境建议
在实际部署时,建议采取组合策略:
基础防护:
- 修改Misfire策略为DO_NOTHING
- 设置合理的资源阈值(CPU/Memory)
- 配置完善的监控告警
高可用部署:
# 多Master配置示例 master: deploy-mode: cluster hosts: - master1 - master2 - master3调度策略优化:
- 避免设置过高的调度频率
- 对重要任务设置任务组优先级
- 合理设置任务超时时间
这个问题的解决过程让我深刻体会到,即使是成熟的开源组件,也需要根据实际业务场景进行定制化调整。特别是在调度系统这种基础架构层面,默认配置往往需要结合业务特点进行优化。