ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker部署XXL-JOB完整指北:从分布式任务调度到时区避坑

Docker部署XXL-JOB完整指北:从分布式任务调度到时区避坑 碰到任务量一多就头疼的团队十有八九最后都会走到 XXL-JOB 这条路上来。这个项目部署起来其实不复杂但网上教程版本混乱有人卡在初始化数据库有人卡在容器起不来还有人把调度中心跑起来了结果发现时区差 8 个小时任务在凌晨三点自动“早退”。这篇文章我按自己实际搭建的过程把 Docker 部署 XXL-JOB 的完整链路捋一遍从选型决策、数据库准备、Compose 编排到高频故障的排查思路一次性讲透。1. 分布式定时任务的需求缺口为什么是 XXL-JOB1.1 定时任务散落各处问题到底出在哪很多团队早期的定时任务都是“哪里有需要就在哪里写”。Java 项目里加Scheduled注解运维在服务器上挂 crontabWindows 机器上再塞几个计划任务。这种模式在只有两三个任务的时候完全没问题但一旦任务多起来痛点会非常明显。第一个痛点是无法统一管理。任务散落在各个服务里谁会定时执行什么只有写代码的人自己清楚。这个人离职之后这些定时任务就变成了“隐形资产”没人敢动。第二个痛点是单点风险。凡是挂在某台固定机器上的 crontab只要这台机器宕机所有调度全部停摆。如果是在多台机器上都部署了同一个服务Scheduled又会重复执行——库存扣减、优惠券发放这类操作多执行一次就是事故。第三个痛点是没有执行记录和告警。任务到底跑没跑成功跑了一次还是两次失败了有没有人通知这些在裸写的定时任务里完全不可控。排查问题时只能翻日志效率极低。XXL-JOB 这类分布式任务调度平台就是为了解决这几个问题而存在的。它的核心价值不是“帮你写任务”而是把“什么时候执行、在哪个机器上执行、执行结果如何、失败了怎么处理”这些问题统一管起来。1.2 XXL-JOB 的两个核心角色与核心能力XXL-JOB 架构上分成两部分调度中心admin和执行器executor。调度中心是管理端提供 Web 管理界面负责任务的创建、定时策略配置、调度触发、执行日志的查看和统计。所有任务的“指挥权”都在这里但它本身不执行业务逻辑。执行器是真正干活的一方嵌入在业务系统里负责接收调度中心的触发请求执行具体的业务代码执行器分为“手动新增”和“自动注册”两种方式一般推荐让执行器自动注册到调度中心运维成本更低。两部分通过网络通信协作调度中心把触发指令推送给执行器执行器执行完把结果回报给调度中心。因为执行器可以部署多台所以天然支持负载均衡和故障转移。核心能力方面XXL-JOB 提供了很多开箱即用的特性丰富的调度策略支持 cron 表达式、固定速度、固定延迟等触发方式路由策略第一个、最后一个、轮询、随机、一致性哈希、故障转移、忙碌转移等决定任务发给哪个执行器失败重试与告警可以配置失败重试次数同时通过邮件等方式推送告警任务分片把一个大数据量的任务拆分成多片分发给不同的执行器并行处理动态管理任务的启停、修改 cron 表达式都可以在管理界面实时生效不需要重启服务。1.3 对比其他方案为什么先学 XXL-JOB定时任务领域常见的选择还有 Quartz、ElasticJob、PowerJob 等。Quartz 是 Java 领域的老牌任务调度库功能成熟但它只是一个库没有管理界面也没有分布式调度中的任务分片、动态管理等能力需要自己二次开发。ElasticJob 出自当当网定位是分布式调度解决方案功能全面但有较强的分片模型约束学习成本和项目结构化要求偏高。PowerJob 是较新的国产分布式调度项目功能很现代文档也比较全但社区规模相比 XXL-JOB 还有差距。XXL-JOB 的优势在于轻量、上手快、社区活跃。部署完调度中心业务系统引入一个 starter配置好执行器就能在后台动态创建任务。在“解决实际问题”这个维度上它是投入产出比很高的选择。就算后面业务复杂度上来了它支持的集群部署和任务分片也足够覆盖大多数场景。2. 部署前的关键决策镜像、数据库、网络一次想清楚2.1 镜像选择与版本匹配XXL-JOB 官方在 Docker Hub 上发布了镜像但版本管理有一个特征大版本之间的配置和页面差异比较大。我建议优先使用官方镜像仓库中的xuxueli/xxl-job-admin版本则根据实际场景选择。目前主流稳定版本集中在 2.3.x 和 2.4.x。2.3.1 使用频率最高网上资料最多遇到问题容易搜到解决方案。2.4.x 更新了后台 UI整体交互更现代同时改进了部分依赖。如果你是利用现有业务系统集成要注意执行器端依赖的xxl-job-core版本号码最好与调度中心版本一致或兼容否则可能出现协议通信不兼容的问题。另外要注意 JDK 环境。旧版本镜像默认基于 JDK8如果你的服务器基础镜像或执行器运行环境是 JDK11、JDK17尽量选择已经适配新 JDK 的版本或自行基于源码构建镜像。这里有一个比较稳妥的思路以官方源码 release 版本为准自己构建一个包含xxl-job-admin的镜像。虽然多一步编译但可以完全掌控版本和时区配置后面我会讲时区问题这个掌控能力很关键。如果不想自己构建直接用官方镜像然后通过环境变量调整时区也是可行方案正文第 4 章会给出具体配置。2.2 数据库规划默认 MySQL 与 PostgreSQL 适配两种路线XXL-JOB 的调度中心运行依赖数据库来保存任务信息、调度日志、执行器注册信息等。官方发布包中提供的是 MySQL 初始化脚本这也是绝大多数团队的默认选择。如果你的公司已经在 PostgreSQL 上建立了统一的数据基础设施希望全部应用都走 PG那就需要走“适配 PostgreSQL”这条路。这里要明确一点XXL-JOB 官方默认并不直接支持 PostgreSQL需要手动将官方提供的 MySQL 脚本改写成 PG 语法并对部分字段类型和 SQL 方言做调整。网上有社区适配版本但版本对应关系必须注意千万不要拿 2.3.1 的适配脚本给 2.4.x 的调度中心用。两种路线的选择建议图省事、团队 MySQL 生态成熟直接走 MySQL。公司有强制 PG 要求或者运维体系以 PG 为核心可以走适配路线但要在测试环境充分验证。后面第 4 章会展开讲适配的改造细节。2.3 Docker 网络与端口规划Docker 部署 XXL-JOB 至少涉及两个容器角色调度中心容器和数据库容器如果数据库也容器化。为了让容器之间通过服务名稳定通信建议创建一个独立的 Docker 网络而不是依赖容器 IP。调度中心默认端口是 8080Web 访问路径是/xxl-job-admin。如果你服务器上 8080 已经被占用可以映射成其他宿主机端口比如8081:8080。执行器则默认使用 9999 端口与调度中心通信。这里要提醒一个容易踩坑的点调度中心访问执行器时使用的是执行器上报的注册地址。如果执行器跑在另一个容器里注册地址不能写localhost要写宿主机 IP 或容器网络内可达的地址如果执行器跑在宿主机上而调度中心在容器里调度中心拿到的执行器 IP 也应该是宿主机 IP而不是 127.0.0.1。网络规划建议网络项推荐配置Docker 网络docker network create xxl-job-net调度中心容器名xxl-job-admin调度中心访问端口宿主机8081映射容器8080MySQL 容器名mysql-xxl-jobMySQL 数据目录挂载宿主机持久化目录执行器注册地址依据部署位置选择宿主机 IP 或容器服务名端口和数据卷在 Compose 文件中就能一并规划好不建议一个个docker run去敲后面我直接给一套完整的 Compose 配置。3. 用 Docker Compose 把调度中心跑起来3.1 准备数据库初始化脚本怎么执行先用官方 SQL 脚本初始化数据库。无论用 MySQL 还是 PostgreSQL这一步都是前提。以 MySQL 为例文件通常位于源码包的doc/db/tables_xxl_job.sql。我们的目的是得到一个名为xxl_job的数据库里面包含调度中心运行所需的全部表结构。如果数据库已经用容器跑起来了可以直接把 SQL 脚本复制进容器执行docker exec -i mysql-xxl-job mysql -uroot -p你的密码 tables_xxl_job.sql如果还没有数据库容器建议先把 MySQL 容器拉起再执行上面的命令。注意执行前要确保 SQL 文件中包含CREATE DATABASE IF NOT EXISTS xxl_job语句如果没有需要手动先创建数据库。执行完成后检查一下核心表是否存在docker exec -it mysql-xxl-job mysql -uroot -p你的密码 -e USE xxl_job; SHOW TABLES;正常情况下会看到xxl_job_group、xxl_job_info、xxl_job_log、xxl_job_logglue、xxl_job_lock、xxl_job_registry、xxl_job_user等核心表。3.2 编写 docker-compose.yml数据库准备好之后用 Docker Compose 一次性把调度中心跑起来。下面是完整的docker-compose.yml示例如果你是 2.3.1 版本且数据库是本机服务也可以去掉mysql服务直接连接宿主机数据库。version: 3.8 services: mysql: image: mysql:5.7 container_name: mysql-xxl-job environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: xxl_job TZ: Asia/Shanghai ports: - 3307:3306 volumes: - /data/mysql-xxl-job:/var/lib/mysql - /path/to/tables_xxl_job.sql:/docker-entrypoint-initdb.d/tables_xxl_job.sql networks: - xxl-job-net command: [ --character-set-serverutf8mb4, --collation-serverutf8mb4_unicode_ci ] xxl-job-admin: image: xuxueli/xxl-job-admin:2.3.1 container_name: xxl-job-admin environment: PARAMS: --server.port8080 --spring.datasource.urljdbc:mysql://mysql:3306/xxl_job?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai --spring.datasource.usernameroot --spring.datasource.passwordroot123456 --xxl.job.accessTokendefault_token --xxl.job.i18nzh_CN TZ: Asia/Shanghai ports: - 8081:8080 volumes: - /data/xxl-job-admin/applogs:/data/applogs depends_on: - mysql networks: - xxl-job-net networks: xxl-job-net: driver: bridge这个配置里有两个细节值得单独说明。第一个是PARAMS环境变量。XXL-JOB 的官方镜像默认启动命令是java -jar /app/xxl-job-admin.jar并允许通过PARAMS环境变量注入额外启动参数。上面的PARAMS覆盖了数据源连接信息、访问令牌、语言等配置这样就不需要改容器内的application.properties非常方便。第二个是depends_on。它只能保证容器启动顺序不代表 MySQL 已经就绪。如果调度中心启动时数据库还没有完全初始化完成会报连接拒绝并自动重试。对于测试环境来说问题不大但最好配合 MySQL 的docker-entrypoint-initdb.d机制上面的配置已体现在数据库容器首次初始化时自动导入 SQL 脚本。3.3 启动、验证与管理后台配置写好后在 Compose 文件所在目录执行docker-compose up -d等待镜像拉取和容器启动完成后查看容器状态docker-compose ps两个容器都处于Up状态后打开浏览器访问http://服务器IP:8081/xxl-job-admin默认账号密码是admin / 123456。如果页面能正常打开说明调度中心已经跑起来了。接着在管理后台左侧菜单的“执行器管理”里应该看不到任何执行器因为业务系统还没有接入。这时候整个平台相当于一个空壳更多价值要靠执行器接入之后才能体现。4. 部署中的高频翻车现场与排查链路4.1 Docker Desktop 启动失败virtualization support not detected很多本机开发环境不是在服务器上部署而是用 Windows 上的 Docker Desktop 起容器。这时候有一个出现频率极高的报错Docker Desktop - virtualization support not detected Virtualization support was detected but it is disabled or not configured properly.这个报错的意思是Docker Desktop 检测不到系统虚拟化能力或者虚拟化功能没有正确开启。排查链路建议按以下顺序走第一步确认 BIOS/UEFI 中的虚拟化开关。重启电脑进入 BIOS找到 Intel Virtualization TechnologyIntel VT-x或 AMD SVM 选项确保为 Enabled。这一步很多人会忽略因为部分品牌机默认是关闭的。第二步确认 Windows 功能里相关的虚拟化组件是否开启。在“启用或关闭 Windows 功能”中勾选“Hyper-V”和“适用于 Linux 的 Windows 子系统”。如果只是勾选了 Hyper-V 而没装 WSL2也可能引发这个报错。建议执行以下命令确认 WSL 状态wsl --status第三步确认 Docker Desktop 使用的后端模式。新版 Docker Desktop 默认使用 WSL2 后端如果你更习惯用 Hyper-V 后端可以在 Docker Desktop 的 Settings - General 中切换。两种模式不要同时混用。还有一种少见但真实存在的情况电脑本身安装了第三方虚拟化软件如 VirtualBox、VMware与 Hyper-V 的虚拟化平台冲突导致 Docker Desktop 无法正常识别。可以考虑卸载或禁用这些软件后再试。排查的顺序很重要先硬件层、再系统层、最后应用层不要一上来就重装 Docker Desktop大部分时候问题在 BIOS 或 WSL2 没有启用。4.2 xxl-job 适配 PostgreSQL 的改造要点如果你的团队因为统一技术栈要求必须把 XXL-JOB 的调度中心跑在 PostgreSQL 上这里有几个关键改造点值得细说。首先明确一点官方没有提供 PostgreSQL 版 SQL 脚本需要手动转换。转换过程中最容易出问题的是数据类型和自增主键。MySQL 中常见的int AUTO_INCREMENT在 PostgreSQL 里要改成SERIAL或BIGSERIAL同时需要为每个表创建对应的序列。更省力的做法是使用 PG 的IDENTITY列GENERATED BY DEFAULT AS IDENTITY语法更现代不需要额外维护序列对象。第二个差异点是日期时间类型。MySQL 的datetime在 PG 里一般改成timestamp注意时区问题。所有与任务调度时间相关的列建议统一使用timestamp without time zone由应用层统一处理时区否则会出现调度记录里的时间与本地时间偏差。第三个差异点是索引和 SQL 方言。个别 SQL 语句在 MySQL 和 PG 中写法不同比如分页查询、字符串截取函数等。如果你拿到的适配脚本是从社区下载的一定要确认里面的xxl_job_log表的查询语句是否针对 PG 做了调整这是最容易出现运行时报错的地方。配置层面调整spring.datasource.urljdbc:postgresql://你的PG地址:5432/xxl_job?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai同时驱动类也要修改并在镜像或启动参数中引入 PostgreSQL JDBC 驱动。特别注意官方镜像中只内置了 MySQL 驱动要用 PG 必须把 PG 驱动 jar 添加进去或者使用自定义构建的镜像否则启动时会直接报找不到驱动类。4.3 时区不对导致任务“没执行”或“提前执行”这个坑是我在实际使用中遇到过的。容器环境的默认时区是 UTC而我们是 UTC8。如果你在 XXL-JOB 后台配置了一个每天凌晨 3 点的任务调度中心会按照容器内的时间来触发等于每天北京时间上午 11 点才执行。问题比较隐蔽的地方在于用 UI 配置任务时页面上显示的 cron 表达式很容易让人以为是本地时间实际上调度中心拿到的系统时间和 cron 判断都基于容器时区。解决方案从两个层面下手第一容器层面设置时区。在docker-compose.yml中给xxl-job-admin服务增加环境变量environment: TZ: Asia/Shanghai第二JVM 层面强制指定时区。在PARAMS启动参数中追加--spring.jackson.time-zoneGMT8 -Duser.timezoneGMT8这里需要注意PARAMS里面设置的是 Spring Boot 的配置参数而-Duser.timezone是 JVM 参数。如果镜像启动脚本不允许追加 JVM 参数建议直接使用环境变量方式。但像前面这个案例在PARAMS中加--spring.jackson.time-zone就足够覆盖 JSON 序列化的时间偏差而调度核心的时间判断主要依赖 JVM 默认时区所以最稳的方式还是把TZ与启动参数一起配。执行器容器同理也要设置时区否则会出现“调度中心记录显示任务已触发但执行器侧执行时间和预期不一致”的情况。4.4 容器依赖顺序数据库没就绪调度中心疯狂重试前面 Compose 配置里写了depends_on: mysql但depends_on只表示依赖服务的容器启动了并不表示 MySQL 已经完成初始化和可以接受连接。如果 MySQL 是首次初始化需要在容器内部执行建库建表脚本这个耗时可能几十秒。这期间调度中心启动了连接数据库会报错并反复重试。有些版本会在日志里出现大量连接异常堆栈看起来非常吓人。针对这个问题有两种处理方式。一种是用脚本做健康检查等数据库真正就绪后再启动调度中心depends_on: mysql: condition: service_healthy healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10另一种更务实的方式就是利用数据库镜像的docker-entrypoint-initdb.d机制在数据库容器首次启动时自动执行初始化脚本。这样数据库一旦完成初始化表结构就已经存在。配合depends_on的service_healthy条件可以大幅降低偶发的连接失败问题。5. 从“能跑”到“好用”执行器接入与集群进阶5.1 执行器如何接入Spring Boot 集成示例调度中心上线后真正的工作是让业务系统接入进来。官方提供了xxl-job-spring-boot-starter把集成成本降到了很低。在 Spring Boot 项目的pom.xml中引入依赖dependency groupIdcom.xuxueli/groupId artifactIdxxl-job-core/artifactId version2.3.1/version /dependency在配置文件中添加执行器相关配置xxl.job.admin.addresseshttp://localhost:8081/xxl-job-admin xxl.job.accessTokendefault_token xxl.job.executor.appnamemy-service-executor xxl.job.executor.address xxl.job.executor.ip xxl.job.executor.port9999 xxl.job.executor.logpath/data/applogs/xxl-job/jobhandler xxl.job.executor.logretentiondays30这里逐行解释一下配置的含义。admin.addresses是调度中心的 Web 地址执行器启动后会往这个地址自动注册。accessToken必须与调度中心配置的 token 一致相当于握手密钥。executor.appname是执行器在调度中心显示的名字同一个业务服务的多个实例共用同一个 appname才能被调度中心识别为一组执行器进行负载均衡或故障转移。executor.port是执行器自身监听的端口用于接收调度请求。executor.logpath是任务执行日志的写入目录这个目录一定要在容器或宿主机上持久化否则任务执行记录会随着容器重建而丢失。配置好之后还需要一个配置类创建调度执行器组件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.port}) private int port; Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); executor.setAppname(appname); executor.setPort(port); executor.setAccessToken(accessToken); return executor; } }注意如果你的 Spring Boot 版本较高2.x 较新版本上面依赖可能用的是xxl-job-spring-boot-starter而不是xxl-job-core两者标签不同starter 方式会在自动装配阶段处理好配置绑定。具体以官方文档为准。业务侧写一个任务处理器Component public class SampleJobHandler { XxlJob(demoJobHandler) public void demoJobHandler() throws Exception { System.out.println(XXL-JOB demo job running...); } }启动服务后回到调度中心后台“执行器管理”应该能看到自动注册的执行器。然后到“任务管理”里新建任务Cron 表达式中配置触发时间JobHandler写demoJobHandler保存后启动任务即可。任务调度记录和执行日志都能在后台看到。5.2 调度中心多节点集群怎么部署当任务量增加或者调度中心需要保障高可用时可以部署多个调度中心节点。XXL-JOB 的调度中心集群设计比较简单多个节点共享同一个数据库节点之间没有其他状态需要同步通过数据库行锁机制保证同一时刻一个任务最多被一个节点触发。部署方式就是在多台机器上各起一个相同的调度中心容器数据库连接同一个注意不要使用不同的初始化数据或本机内存差异配置。可以把多个调度中心节点挂到同一个负载均衡后面统一对外提供地址执行器配置中的admin.addresses可以写负载均衡的 VIP 地址。有一点要注意调度中心使用内存中的 Quartz 线程池集群模式下各个节点可能同时抢任务最终靠数据库分布式锁兜底完成互斥。所以不用担心多个节点导致任务重复执行。执行器本身也可以部署多节点这是分布式调度最常见的形态。在调度中心新建任务时路由策略选“轮询”或“一致性哈希”任务触发后会按照策略分发到其中一个执行器节点。如果选“故障转移”当某个节点异常时自动切换到其他健康节点。5.3 日志、报警与任务治理的落地建议平台跑起来只是第一步真正决定使用体验的是运维细节。日志方面调度中心自身的日志和任务执行日志是分开的。前者是调度中心容器内的应用日志即/data/applogs目录后者是执行器节点上每个任务执行时产生的日志由执行器配置中的logpath决定。建议两者都挂载到宿主机持久化目录并配置集中式日志采集。告警方面XXL-JOB 内置了任务失败报警机制默认通过邮件发送。在调度中心后台配置邮件 SMTP 服务后任务触发失败或执行失败时会收到邮件通知。在规模较大的团队里可以基于失败回调做企业微信或钉钉机器人告警。2.4.x 版本已经提供了较为灵活的通知方式具体以对应版本文档为准。任务治理方面有几个经验可以分享。一是在创建任务时养成约定任务名称前缀的习惯比如按业务模块命名order-sync、report-generate同时保证JobHandler与任务描述一一对应后期才能快速定位。二是合理设置失败重试次数。重试太多会造成业务重复执行比如发短信、转账类任务必须在任务处理器内做好幂等控制重试太少又可能偶发网络抖动导致漏跑。一般建议重试次数不超过 3 次核心任务配合手动重新触发。三是分片任务要注意边界。XXL-JOB 的路由策略支持分片广播即每个执行器节点都会收到一个分片参数在任务处理中使用ShardingUtil.getContent()拿到当前分片编号和总分片数自己根据业务键取模或分段来实现并行处理。这里最容易犯的错是直接把分片数理解成固定值实际上节点上下线时分片数会变化业务逻辑要对这种变化有容忍度。四是我个人维护这套平台时的一个习惯每周检查一次调度失败日志不仅看失败任务也看执行耗时突增的任务。调度平台只是工具任务本身的性能瓶颈才是需要持续关注的核心。最后说一个小细节如果你的执行器跑在容器里并且使用了 Kubernetes 这样的动态调度平台执行器 IP 会频繁变化。建议在配置里不要固定executor.ip而是开启自动注册让执行器启动时把当前 Pod 的 IP 上报给调度中心。如果 K8s 网络和节点网络存在 NAT 场景则需要单独规划一段固定映射地址这属于 K8s 网络设计范畴这里不展开。整个平台从部署到稳定运行我自己的体会是先把基础组件跑通再逐步接入业务不要一上来就上集群和分片。毕竟任务调度平台最核心的价值是让定时任务变得可控、可观测、可治理而不是把架构搞得越复杂越好。合理规划容器网络、时区、日志持久化这些细节这套平台用起来会顺手很多。
返回列表