尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于xxl-job搭建分布式任务调度中心:Docker部署与Spring Boot执行器实战

基于xxl-job搭建分布式任务调度中心:Docker部署与Spring Boot执行器实战 如果你所在的团队还停留在定时任务靠人肉跑的阶段——每天早上登录服务器手动执行脚本或者干脆在Spring Boot里用Scheduled写死那么这个项目值得你花5分钟看完基于xxl-job搭建一套统一的分布式任务调度中心用Docker把调度中心部署起来然后把Spring Boot服务作为执行器接入从此定时任务有了统一管理入口、失败重试和完整日志追踪。这篇文章不是简单地贴几步命令我会把原理、步骤、配置项含义、以及我踩过的坑全部拆开讲。你照着操作不只是跑通而是真正理解这套调度体系在项目里该怎么用。1. 从Scheduled到xxl-job手动任务到底坑在哪1.1 单机Scheduled的三个死穴相信很多人刚接触定时任务时第一反应都是Spring Boot自带的Scheduled。确实如果是个人项目或者单机小服务Scheduled cron表达式已经够用了配置简单、不用额外装东西、跑起来也没毛病。但当我开始接手一个总共5个微服务、双环境部署的项目时情况立刻变了。第一个死穴是集群执行问题。服务一上多节点Scheduled在每个节点上都会执行一遍同一个任务被重复触发。数据同步就是重复导入轻则埋点数据多算重则库存表直接翻倍。你可能会说加个分布式锁不就解决了吗但分布式锁只能解决同时只有一个节点执行的问题任务日志分散在各台机器、没有统一的重试和告警机制这些运维问题依然存在。第二个死穴是监控与失败重试。Scheduled抛异常默认只是打印一条日志没有持久化的执行记录。任务半夜2点失败你第二天早上开晨会的时候根本不知道等上游数据对不上账去查日志可能已经过去了十几个小时。要自己做失败补偿、任务追跑、执行状态看板代码量会迅速失控而且这些代码和业务逻辑纠缠在一起后面没人敢动。第三个死穴是任务运维的割裂。业务任务散落在各个服务里没有统一的管理入口。产品说这个任务今晚要手动跑一次你需要拿Postman调接口或者写一个临时的controller任务A执行完要通知任务B你又要写一堆链式调用逻辑。这些本质上都是调度功能不应该散落在业务代码里自己管理。1.2 xxl-job的角色拆分调度中心、执行器、任务日志这个时候就需要一个独立的调度平台。xxl-job是我用下来最主流的开源方案之一社区活跃部署成本可控。它的核心模型是三个角色分开调度中心xxl-job-admin不执行业务代码只负责按Cron或固定频率生成调度请求维护任务配置、执行记录、告警。它是一个独立的Spring Boot应用可以单独打镜像部署。执行器executor嵌入在你的Spring Boot应用里负责接收调度中心发来的HTTP请求并执行任务逻辑然后把执行结果和日志上报回调度中心。任务JobHandler在代码里通过XxlJob(名字)标注的具体方法是真正跑业务逻辑的地方。理解这个模型非常关键。调度中心与执行器之间通过HTTP协议通信执行器启动时会向调度中心注册自己的IP和端口调度中心按任务配置的路由策略把请求分发到具体节点。整个过程里业务代码不需要感知谁是调度者只需要把自己的处理逻辑暴露成JobHandler并处理分片参数。就因为这个角色拆分团队在集群环境下不再有重复执行的问题一份任务配置只会在调度中心上触发一次具体落到哪个执行器由路由策略决定路由策略选对了就能精确控制执行行为。2. Docker部署调度中心数据库初始化与镜像启动的完整走读2.1 第一步准备好MySQL并导入官方表结构xxl-job-admin是一个Spring Boot应用持久化依赖MySQL。官方镜像并不会帮你自动建表所以第一件事是准备数据库。版本选择上MySQL 5.7和8.0都可以我用的是8.0注意连接串里要加上时区参数。首先创建数据库CREATE DATABASE xxl_job DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后拿到官方初始化脚本tables_xxl_job.sql。这个脚本在xxl-job仓库的doc/db目录下也可以直接从镜像里copy出来更方便docker run --rm xuxueli/xxl-job-admin:2.4.0 cat /app/tables_xxl_job.sql tables_xxl_job.sql这里有个实践经验镜像自带sql脚本直接copy比去GitHub翻更不容易拿错版本。拿到脚本后导入mysql -h127.0.0.1 -uroot -p xxl_job tables_xxl_job.sql导入后用SHOW TABLES确认核心表是否齐全至少能看到xxl_job_info、xxl_job_log、xxl_job_registry、xxl_job_group这几张表。xxl_job_info是任务配置表xxl_job_log是调度与执行日志表xxl_job_registry是执行器在线注册表xxl_job_group是执行器分组表。这几张表后期排查问题都会用到。2.2 第二步用docker run快速拉起调度中心数据库就绪后启动调度中心容器。最精简的做法是一条docker run命令docker run -d \ --name xxl-job-admin \ -p 8080:8080 \ -p 9999:9999 \ -e PARAMS--spring.datasource.urljdbc:mysql://192.168.1.100:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/ShanghaiautoReconnecttrue --spring.datasource.usernameroot --spring.datasource.password你的密码 --xxl.job.accessTokendefault_token \ xuxueli/xxl-job-admin:2.4.0几个参数需要特意说明。PARAMS是JVM启动参数镜像里做了包装直接拼成Spring Boot的配置项。URL里serverTimezoneAsia/Shanghai这个很重要缺了会造成时间差8小时useSSLfalse可以避免MySQL连接时的一堆告警。8080是admin界面的HTTP端口9999是执行器回调端口两个都要映射出来。如果你希望更好管理建议用docker-compose把MySQL和admin放在同一个网络里version: 3 services: mysql: image: mysql:8.0 container_name: xxl-job-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: xxl_job command: --default-authentication-pluginmysql_native_password --character-set-serverutf8mb4 ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql - ./tables_xxl_job.sql:/docker-entrypoint-initdb.d/tables_xxl_job.sql:ro xxl-job-admin: image: xuxueli/xxl-job-admin:2.4.0 container_name: xxl-job-admin depends_on: - mysql ports: - 8080:8080 - 9999:9999 environment: PARAMS: --spring.datasource.urljdbc:mysql://mysql:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai --spring.datasource.usernameroot --spring.datasource.passwordroot123 --xxl.job.accessTokendefault_token volumes: mysql-data:把tables_xxl_job.sql放到和compose文件同级目录后docker-compose up -dMySQL首次启动时会自动初始化数据库并导入脚本这才是真正的一条命令完成。这里要留意MySQL 8.0的密码认证插件问题如果你后续要用宿主机上的客户端连接加--default-authentication-pluginmysql_native_password更省心。2.3 第三步登录后台创建执行器验证部署成功容器起来后访问http://localhost:8080/xxl-job-admin默认账号admin密码123456。登录成功后第一件事不是急着集成Spring Boot而是先把执行器分组建好。执行器管理页面点新增执行器AppName填xxl-job-executor-sample要和Spring Boot配置的appname完全一致名称随便写注册方式选自动注册机器地址先不填。保存后先不用管等执行器启动后会自动注册上来。到这里调度中心就部署好了。如果打开任务管理页面能看到新增任务按钮、数据列表能正常加载说明数据库连接和表结构都正常。下一步就是把Spring Boot应用的执行器接进来。3. Spring Boot执行器集成的三件套依赖、配置类、任务代码3.1 Maven依赖与版本强一致规则集成执行器其实就是在你自己的Spring Boot服务里引入xxl-job-core再配一个XxlJobSpringExecutor的Bean。POM里加dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.4.0/version /dependency这里有一个必须强调的硬规则执行器的xxl-job-core版本与调度中心镜像版本必须一致。比如admin用2.4.0客户端就用2.4.0的依赖。版本不一致会出现序列化兼容问题轻则任务执行没反应重则直接报Handler not found。我见过有人admin用2.4.0、客户端引2.3.1结果任务能注册上、但触发时报方法找不到花了一个下午排查才意识到是版本错位。3.2 配置类和application.yml逐字段拆解接下来是执行器的装配。我在项目里习惯用一个专门的配置类把参数通过Value注入避免把配置写死Configuration public class XxlJobConfig { Value(${xxl.job.admin.addresses}) private String adminAddresses; Value(${xxl.job.accessToken}) private String accessToken; Value(${xxl.job.executor.appname}) private String appname; Value(${xxl.job.executor.ip}) private String ip; Value(${xxl.job.executor.port}) private int port; Value(${xxl.job.executor.logpath}) private String logPath; Value(${xxl.job.executor.logretentiondays}) private int logRetentionDays; Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); executor.setAppname(appname); executor.setIp(ip); executor.setPort(port); executor.setAccessToken(accessToken); executor.setLogPath(logPath); executor.setLogRetentionDays(logRetentionDays); return executor; } }application.yml对应的配置xxl: job: admin: addresses: http://localhost:8080/xxl-job-admin accessToken: default_token executor: appname: xxl-job-executor-sample ip: port: 9999 logpath: /data/applogs/xxl-job/jobhandler logretentiondays: 30逐个拆解一下admin.addresses是调度中心的完整访问地址注意一定要带上下文路径/xxl-job-admin如果漏了这个后缀执行器注册和任务回调全都不通。accessToken是调度中心和所有执行器的共享令牌两边不一致时admin端不会接收该执行器的注册请求。生产环境务必改成强随机串。executor.appname是执行器分组标识必须和admin后台手动创建的执行器AppName一模一样否则你在admin里创建任务时根本找不到该执行器。executor.ip一般留空框架会自动探测本机IP。只有当自动探测到内网IP而调度中心无法访问典型场景是容器跨宿主机通信时才手动指定。executor.port是执行器自带的HTTP服务端口默认9999。调度中心后续要通过这个端口反向触发执行器所以生产环境防火墙一定要放行这个端口。logpath和logretentiondays是业务日志落盘路径及保留天数。日志路径要保证有写权限容器部署建议挂载到宿主机目录。3.3 写第一个XxlJob任务并手动触发配置完执行器后写一个JobHandler。以从第三方接口同步数据为例Component public class DataSyncJobHandler { private static final Logger logger LoggerFactory.getLogger(DataSyncJobHandler.class); XxlJob(dataSyncJobHandler) public void dataSync() { XxlJobHelper.log(数据同步任务启动); // 模拟业务逻辑这里可以是拉取接口、增量入库、清理过期数据等 int total 0; for (int i 0; i 100; i) { total i; try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } XxlJobHelper.log(同步完成处理记录数: {}, total); } }XxlJob注解里的字符串就是JobHandler名称它在同一个调度中心下要全局唯一。任务里记录日志时用XxlJobHelper.log而不是logger.info因为前者会把日志主动上报给调度中心你在admin的调度日志详情页里就能直接看到任务内部输出而后者只能去服务器上翻日志文件。这是很多新手容易忽略的差异。写好任务后重启服务再到admin后台刷新执行器管理页面能看到刚才配置的xxl-job-executor-sample状态变为在线。然后去任务管理新增任务运行模式选BEANJobHandler填dataSyncJobHandlerCron表达式随便填一个未来时刻先保存。接下来点执行一次进入调度日志页面如果看到执行成功并且日志里有同步完成处理记录数说明整个链路已经打通。4. 任务调度策略解密Cron、路由、阻塞、重试怎么搭配4.1 Cron表达式6位字段还是7位秒必须写xxl-job和Quartz一致使用6位Cron表达式秒 分 时 日 月 周。很多从Linux crontab过来的人会犯一个经典错误Linux的cron是分 时 日 月 周5位没有秒直接把表达式抄过来就等于整体前移了一位结果任务在完全意料之外的时间触发。我整理几个常用的表达式执行频率Cron表达式6位说明每天凌晨2点0 0 2 * * ?注意最后用问号而不是星号每5分钟0 */5 * * * ?会整点对齐地跑每小时30分0 30 * * * ?每天跑24次工作日9点0 0 9 * * MON-FRI周字段用MON-FRI每月1号0点0 0 0 1 * ?月初清理类任务常用有两个字段需要重点说明。日和周不能同时指定值要么日留问号、周写值要么日写值、周留问号两个都写值表达式直接非法。另外秒位必须写之前有人写0 0 2 * * ?实际触发时间是每天2点0秒这是对的但如果写成0 2 * * ?就变成了每小时的第0分第2秒也就是每小时都会跑一次这正是我强调秒的原因。4.2 路由策略数据同步场景下的选择逻辑路由策略解决的是多个执行器节点这次触发谁的问题。xxl-job的策略非常多但实际项目里高频使用的就那么几个。轮询任务在多个节点间轮流跑。适合无状态、任意节点执行结果一致的场景。一致性HASH同一个JobHandler参数会路由到同一个节点。适合每个节点有本地缓存、希望同一任务固定打在同一台机器上的场景。故障转移触发失败自动切换下一个节点。适合对执行成功率要求较高、且任务本身轻量的场景。分片广播所有节点同时执行。配合XxlJobHelper.getShardIndex()当前分片索引从0开始和getShardTotal()总分片数把数据按ID取模拆分到每台机器上并行处理。这是大数据量数据同步任务最常用的方案。比如之前有个订单数据清理任务单机跑需要40分钟我改成4个执行器节点 分片广播每个节点只处理订单ID取模后属于自己分片的数据运行时间直接缩短到12分钟左右。代价是任务需要能被拆成多个分片也就是业务数据必须有可分片的维度比如按ID区间、按租户、按时间范围。4.3 阻塞处理与失败重试的搭配建议阻塞处理策略指前一个任务还没执行完下一个调度周期又到了这时候怎么办。三个选项区别很大单机串行后一次调度排队等前面的跑完再执行。这是最稳的选择绝大多数业务任务都应该用它。丢弃后续调度后一次触发直接丢弃防止任务堆积。适合对实时性不敏感、下次跑可以覆盖本次数据的任务比如全量同步类型的。覆盖之前调度强制终止正在跑的执行新的。这个有数据安全风险要非常谨慎不是所有任务都能被安全终止的。失败重试次数这块需要区分清楚。xxl-job里的失败重试次数其实是指调度失败后的重试注意重试场景下任务逻辑必须幂等。我遇到过把重试次数配成3结果回调接口偶发超时导致任务实际已经执行成功但admin判定失败又重试了三次数据被重复插入。最后发现业务方忘了在插入前做唯一键校验。所以凡是开了重试的任务第一要求就是幂等。还有一个很容易忽略的组合建议给数据同步类任务配上超时时间。比如第三方接口响应慢任务卡住超过10分钟还没返回xxl-job会判定执行超时并终止。我推荐任务超时时间不要短于你业务峰值耗时的1.5倍但要短于调度周期否则会出现上次卡着还没超时、下次又要开始的混乱局面。5. 集成后的实战排错执行器注册失败与日志定位链路5.1 执行器状态一直离线三分钟定位链路这个是集成阶段出现频率最高的问题admin后台执行器一直显示离线任务创建后没有节点可选。我的排查顺序是固定的。第一步看执行器启动日志重点搜xxl-job register或者registry关键字。如果看到连接超时十有八九是admin.addresses配错了或者调度中心的8080端口没通。用curl验证一下curl http://调度中心IP:8080/xxl-job-admin能返回页面说明地址本身OK。第二步看appname是否一致。admin后台执行器管理里新建的AppName和Spring Boot配置文件里的xxl.job.executor.appname必须一字不差。很多离线问题其实是执行器分组的命名对不上admin找不到对应的注册节点。第三步看accessToken。两边令牌不一致执行器的注册请求会被调度中心直接忽略。而且xxl-job的默认配置不是完全关闭鉴权而是默认default_token这个值会同时出现在admin和executor的默认配置里所以两边都不改是可以通的。但如果生产环境要求改admin端token记得同步把每个服务的accessToken都改掉。第四步是网络和端口。执行器主动注册是往admin推但后续admin触发任务需要反向访问执行器的9999端口两者方向不同。容器场景经常出现执行器显示在线但任务触发失败就是只放了8080忘了放9999。跨进程排查时用netstat或docker logs确认执行器端口真实监听状态。5.2 任务被调度但没执行的日志排查顺序任务在任务管理里显示调度成功但业务代码里没有走出任何日志这种问题比注册离线更难察觉。按下面的顺序查第一确认运行模式。任务运行模式选的是BEAN还是GLUEBEAN模式要求JobHandler名字与XxlJob注解的值完全匹配注意JobHandler名区分大小写并且在任务编辑页里的JobHandler输入框要填对。GLUE模式则是代码在线维护一旦选错执行器上根本没有对应处理器。第二确认执行器在线且路由策略有节点可选。如果路由策略选了第一个但注册节点列表为空触发请求也会报错。这时候可以临时把路由策略改成轮询再手动触发一次看有没有起效。第三进入调度日志详情。admin的任务管理里每条调度记录点开能看到调度结果和执行结果。如果调度成功但执行结果为空问题大概率在执行器侧如果连调度日志都没有说明Cron没到点或者任务被设置成停止状态。第四去执行器的日志目录里翻jobhandler日志。XxlJobHelper.log上报的是结构化日志而应用自己的logger.info只写在本地。这个日志文件就是任务执行的真实证据。容器环境如果logpath没有挂载出去容器重建后日志丢失所以docker-compose里务必要把日志目录映射到宿主机。5.3 容器环境的时区、端口与日志持久化问题跑在Docker里的执行器和调度中心和跑在物理机上有几个差异点不处理就会出一些很诡异的故障。时区是最典型的。很多基础镜像默认UTC时区如果你没有在启动容器时设置TZ环境变量任务显示的调度时间会比北京时间差8个小时。之前有个客户反馈任务每天8点跑实际是每天16点才跑查到最后就是镜像时区问题。解决方案很粗暴docker run时加-e TZAsia/Shanghai或者docker-compose的environment里写TZ。端口分配要注意执行器端口冲突。同一个宿主机上跑多个Spring Boot服务如果每个执行器都默认9999后启动的会Bind失败服务直接起不来。我习惯给每个服务分配不同端口比如9999、9998、9997并在admin后台分别建执行器分组这样调度中心就能精确定位到每一个应用实例。日志持久化是最后一个容易被忽略的点。XxlJobHelper的日志默认写在logpath路径如果你不把日志目录挂载成volume容器一回收所有历史执行日志就全没了。管理员在排查历史任务失败原因时就会很被动。经验是docker-compose里加volumes映射把logpath指到宿主机的/data/logs/xxl-job/ /目录下再配合定期清理策略既方便排查又不会把磁盘打满。其实我在实际使用中还发现一个很小的习惯调整对整个排查效率帮助很大任务日志里第一行固定打任务入参第二行打开始时间结束前打耗时。这样无论从admin里看还是去日志文件里翻你都能在几秒内判断任务到底是什么时候跑的、跑了多久、在哪一步慢的。配任务报警邮箱也是正式环境的刚性需求否则任务凌晨失败没人知道到早上才发现数据缺了一块那整个晚上的下游结算都会受影响。另外如果你后续要接告警通知建议优先用企业微信Webhook或者邮件xxl-job自带的报警扩展点本身就很轻量不要自己再包一层重型消息中间件。把这几个细节都处理干净xxl-job这套东西就能从能跑变成好用。
返回列表