Firefly:为 Java 业务系统打造轻量级分布式调度中心
Firefly:一个轻量、清晰、面向 Java 业务系统的调度中心
摘要:很多业务系统最后都会长出一套“定时任务治理”需求:任务要能集中管理,执行器要能远程注册,失败和超时要能观察,跨时区任务不能被机器默认时区带偏。Firefly 尝试用一个 Java 21 的轻量调度服务,把这些能力拆成清晰的 core、store、transport、API 和 UI 边界。
Tags: Java, Spring Boot, Scheduler, Distributed Systems, Firefly
项目入口:官方网站(GitHub Pages) · GitHub 仓库 · 快速开始 · 集成文档 · Admin API · 问题反馈
为什么还需要一个新的调度服务
定时任务一开始往往很简单:一个 @Scheduled,一个 cron,一个业务方法。但系统规模上来以后,问题会变得具体而麻烦:
- 任务散落在多个服务里,谁在线、谁接了任务不清楚。
- 业务服务不想暴露端口,但调度中心又需要远程触发它。
- cron 看起来没问题,但跨时区、夏令时和机器默认时区会把任务悄悄带偏。
- 一次执行失败后,到底是没派发、没 ACK、超时、业务失败,还是重试耗尽,很难追。
- 集群里多个调度节点同时工作时,需要明确的 ownership 和 fencing。
Firefly 的目标不是做一个大而全的工作流平台,而是做一个轻量调度中心:核心调度语义保持克制,远程执行、持久化、管理 API、控制台和指标都放在独立模块里。
核心思路:把调度系统拆成几个清楚的边界
| 边界 | 模块 | 关注点 |
|---|---|---|
| 调度核心 | libs/scheduler-core | cron、fixed-rate、任务级 ZoneId、misfire、并发策略 |
| 远程执行 | transports/netty、clients/executor-netty | Executor 主动连接 Gateway、心跳、触发、ACK、结果上报 |
| Spring Boot 集成 | integrations/firefly-spring-boot-starter | 自动配置 Executor、扫描 @FireflyJob、启动同步任务 |
| 持久化与 HA | stores/jdbc | job、execution、outbox、node、shard lease、fencing token |
| 管理 API | apis/admin-http | JSON API、认证、RBAC、审计、任务管理 |
| 运维面 | ui/admin、plugins/metrics-prometheus | Admin 控制台、Prometheus 指标 |

Caption: Firefly 把业务服务、Gateway、调度中心、持久化存储、管理 API 和 Admin UI 分成独立边界。
一个更贴近业务的例子:从“几个 cron”到“可治理的任务”
假设一个账单系统里有三类任务:
- 每天凌晨 2 点生成账单。
- 每小时巡检未完成订单。
- 运维人员偶尔需要手动触发一次补偿任务。
如果这些任务只是散落在不同服务里的 @Scheduled 方法里,短期很省事,长期会出现几个问题:任务定义不集中、执行历史不集中、实例上下线不可见、手动触发和审计很难统一。Firefly 的思路是让业务代码仍然写在业务服务里,但把“任务定义、触发、执行状态、审计、指标”交给调度中心治理。
这也是 Firefly 与单纯进程内调度最大的差异:它不是把业务逻辑搬走,而是把任务生命周期管理起来。
最快的集成方式:Spring Boot + 注解自动创建任务
业务项目引入 starter:
<dependency>
<groupId>com.firefly</groupId>
<artifactId>firefly-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
Gradle:
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation "com.firefly:firefly-spring-boot-starter:1.0.0"
}
最小配置只需要执行器名称、Gateway 地址和 Integration Key:
spring:
application:
name: firefly-example
firefly:
executor:
name: billing-executor
gateway-addresses:
- 127.0.0.1:9700
integration-key: ${FIREFLY_INTEGRATION_KEY}
server:
port: 80
然后在业务方法上声明任务:
import com.firefly.domain.ExecutionContext;
import com.firefly.spring.annotation.FireflyJob;
import org.springframework.stereotype.Component;
@Component
public class BillingJobs {
@FireflyJob(
name = "每日账单处理",
cron = "0 0 2 * * *",
zoneId = "Asia/Shanghai",
groupId = "billing",
parameters = {"tenant=primary"}
)
public void billingHandler(ExecutionContext context) {
System.out.println("executionId=" + context.executionId());
// run business code
}
}
默认情况下,Starter 使用方法全限定名作为自动入口和任务 ID:
com.example.BillingJobs#billingHandler
业务侧不需要再手写全局 jobId 或 handlerName。Spring 应用启动完成后,Starter 会注册本地 handler,并把 @FireflyJob 声明同步到 Firefly Admin API;调度中心到点后通过 Gateway 触发这个 handler。
如果一个方法需要多个调度计划,可以重复声明 @FireflyJob,并用 key 区分:
@FireflyJob(key = "daily", name = "每日账单", cron = "0 0 2 * * *", zoneId = "Asia/Shanghai")
@FireflyJob(key = "hourly", name = "小时账单巡检", cron = "0 0 * * * *", zoneId = "Asia/Shanghai")
public void billingHandler(ExecutionContext context) {
// run business code
}
从注解到自动建任务,启动时发生了什么
@FireflyJob 的关键不是“少写几行配置”,而是把业务方法、远程执行器和调度中心连成一个启动闭环。

Caption: Spring Boot Starter 在应用启动后扫描 @FireflyJob,注册本地 handler,并通过 Admin API 同步任务定义。
流程可以拆成五步:
- Spring Boot 启动业务服务。
- Starter 扫描
@FireflyJob方法,并生成FireflyJobRegistration。 - Netty Executor Client 主动连接 Firefly Gateway。
- Starter 调用 Admin API 查询任务是否存在。
- 任务不存在时自动创建;任务已存在时默认保持线上配置不变。
这种模式适合“代码声明任务,但线上保留治理权”的团队:开发者用注解表达意图,运维和平台侧仍然能在 Admin UI 里看见任务、节点、执行记录和插件状态。
为什么强调任务级时区
Firefly 要求 cron 任务显式声明 IANA ZoneId,运行态游标统一使用 UTC Instant。这能避免调度行为被部署机器的默认时区影响。
JobDefinition job = JobDefinition.builder()
.id("new-york-daily-report")
.name("New York Daily Report")
.handlerName("reportHandler")
.schedule(new CronSchedule("0 0 9 * * *"))
.zoneId(ZoneId.of("America/New_York"))
.build();
这表示任务会按纽约本地时间 09:00 触发,而不是按调度服务器所在机器的默认时区触发。
集群、Outbox 和可观测性
作为独立调度中心运行时,Firefly 可以使用 JDBC 存储承载任务定义、运行游标、execution、outbox、节点状态和审计记录。
几个关键设计点:
- Scheduler 节点通过 shard lease 获取分片所有权。
- lease 续期和接管使用 fencing token,避免旧 owner 继续推进任务。
- 任务游标 CAS、execution 创建和 outbox 写入在同一个事务中完成。
- 远程派发通过 outbox 记录重试,ACK 超时后可以重新派发。
- Prometheus 插件暴露调度延迟、执行耗时、Executor 连接、数据库时钟偏移等指标。
这让 Firefly 的关注点从“到点执行一个方法”扩展到“调度中心如何在失败、重启、网络抖动和集群接管中保持可解释”。

Caption: Firefly 用任务级 ZoneId 计算业务时间,用 UTC Instant 保存运行游标,并通过 outbox 可靠派发远程执行。
可以把它理解成两条线:
- 时间线:cron 按任务自己的
ZoneId计算,运行态统一落到 UTCInstant。 - 派发线:任务到期后,调度中心在同一事务里推进游标、创建 execution、写入 outbox,再由 Gateway 派发给在线 Executor。
这能减少两类常见问题:一类是“时间算错了”,另一类是“状态推进了但任务没发出去”。Firefly 把这两件事都显式建模,调试时就有地方查。
和常见方案怎么区分
| 方案 | 适合场景 | Firefly 的差异 |
|---|---|---|
| @Scheduled | 单应用内少量任务 | Firefly 提供集中管理、远程执行器、执行历史和集群治理 |
| Quartz | 进程内复杂调度 | Firefly 更强调调度中心、Admin API、Netty Executor 和 JDBC HA |
| XXL-JOB / PowerJob | 成熟任务平台 | Firefly 更轻,核心边界更克制,更适合想自己掌控演进路径的 Java 团队 |
| Airflow / DolphinScheduler | DAG、数据编排、补数 | Firefly 不要求把业务任务建模成 DAG,更贴近服务内业务方法 |
它适合什么场景
Firefly 更适合这些场景:
- Java / Spring Boot 业务系统,希望用注解声明任务,但又想集中治理。
- 业务服务不想被动暴露端口,而是主动连接调度中心。
- 任务涉及跨时区,不能依赖机器默认时区。
- 需要 Admin UI、Admin API、Prometheus 指标和审计记录。
- 希望从单机、本地 H2、PostgreSQL/MySQL 到多节点集群逐步演进。
它不试图替代大型工作流平台。如果你的核心需求是复杂 DAG、数据血缘、补数、人工审批流或数据平台级编排,Airflow、DolphinScheduler 这类系统可能更合适。
快速体验
启动 Firefly Server:
.\gradlew.bat :server:launcher:run --args="--firefly.config.profile=h2"
默认地址:
| 服务 | 地址 |
|---|---|
| Admin UI | http://127.0.0.1:9720 |
| Admin API | http://127.0.0.1:9710 |
| Metrics | http://127.0.0.1:9711/metrics |
| Gateway | 127.0.0.1:9700 |
如果 starter 依赖还没有发布到远端仓库,可以先本地发布:
.\gradlew.bat publishToMavenLocal
发布时可以强调的三句话
如果你要向团队或社区介绍 Firefly,可以抓住这三句话:
- 业务代码还在业务服务里,任务治理交给调度中心。
- Executor 主动连接 Gateway,业务服务不需要为了被调度而暴露端口。
- 时间语义、运行游标、Outbox、shard lease 都显式建模,调度问题更容易解释和排查。
生产接入检查清单
- 任务是否显式声明了 IANA
ZoneId。 - 业务 handler 是否基于
executionId或业务主键保证幂等。 - Integration Key 是否通过环境变量或密钥系统注入。
- Admin API、Gateway、Metrics 端口是否按环境隔离。
- PostgreSQL/MySQL schema 初始化策略是否明确。
- Prometheus 指标和告警是否覆盖调度延迟、Outbox 停滞、Executor 连接和数据库时钟漂移。
结语
Firefly 的价值不在于把所有调度平台能力一次性堆满,而在于把最容易混乱的边界先理清楚:时间语义、执行器连接、持久化状态、集群 ownership、管理 API 和运维可观测。
对于正在从“服务里几个定时任务”走向“统一调度中心”的 Java 团队,Firefly 提供了一条相对轻量、渐进、可理解的路径。
本文来自博客园,作者:青柠_fisher,转载请注明原文链接:https://www.cnblogs.com/oldEleven/p/22078893

浙公网安备 33010602011771号