简介:面向需要在 Kubernetes 集群中落地定时任务调度的运维与后端开发人群,这份 XXL-JOB 容器化部署 YAML 可作为直接应用的部署清单,重点解决集群环境下任务调度组件编排与快速验证的问题。压缩包内仅含 1 个 YAML 文件,类型为 yaml,体积约 782B,内容紧凑,便于按需修改镜像、命名空间与资源配置后提交集群。已有 647 人学习下载,说明其在实际部署场景中具备一定参考价值。读者可直接获得一份经过验证的集群部署配置,用于理解 XXL-JOB 在 K8S 中的服务、容器与调度组件组织方式,并作为环境搭建、参数对照和部署排错的起点,适合具备基础 K8S 操作经验、希望缩短验证路径的读者使用。
1. 为什么先在 K8s 里跑通 XXL-JOB:一个版本坑换来的教训
上个月接手一个 XXL-JOB K8s 集群部署的烂摊子,调度中心 Pod 反复 CrashLoopBackOff,执行器在管理页面里挂着一排“离线”,业务方以为任务在跑,实际三天没触发。排查两小时,根因是 YAML 里镜像 tag 写成了 latest,节点上缓存的旧镜像和新配置对不上。这类 xxljob 容器化部署的活儿,最怕的就是“YAML 文件直接部署成功”这句话被当成结论——apply 返回 created 不等于系统能跑。
XXL-JOB 的结构不复杂,一个调度中心 admin、若干执行器 executor、一个 MySQL。但搬到 K8s 之后,注册地址、时区、探针路径、镜像拉取策略每一项都能单独翻车。这篇装过的记录,适合正准备把调度中心塞进集群的人,也适合已经 apply 完但页面打不开、执行器注册不上的情况。
2. 部署前把环境列清楚:K8s 版本、镜像 tag 和五个 YAML 的分工
动手之前先把版本和资源对一遍,这一步省掉,后面所有报错都会变成黑匣子。
2.1 集群版本与命名空间的硬性检查
XXL-JOB 本身的 YAML 只用到 Deployment、Service、ConfigMap 这几类基础资源,对集群版本不挑。真正会卡人的是 Ingress 和 RBAC 这两块:K8s 1.22 之后extensions/v1beta1和networking.k8s.io/v1beta1的 Ingress 全部下线,一份两三年前下载的 YAML 直接 apply 会报no matches for kind "Ingress";1.25 之后 PodSecurityPolicy 被移除,如果你的 YAML 里还挂着 PSP 引用,整个 apply 会中断。
我一般先跑这四条确认底子:
# 客户端与服务端版本,重点看 Minor 是否 >= 22 kubectl version --short # 节点状态和容器运行时,确认没有 NotReady 节点 kubectl get nodes -o wide # 确认 Ingress API 用的是 v1 kubectl api-resources | grep ingress # 建独立命名空间,别和业务挤在 default kubectl create namespace xxl-job kubectl config set-context --current --namespace=xxl-job参数上没什么可调的,重点是把--namespace固定下来。后面所有命令都默认落在这个命名空间,能避免“资源建到默认命名空间去了”这种低级但高频的问题。如果集群开了 NetworkPolicy 默认拒绝,还要提前放通 admin 的 8080 和执行器的 9999,否则注册心跳会被静默丢掉,页面上就是一直离线。
资源配额也要看一眼。调度中心本身不重,但我见过给 256Mi limit 的,JVM 堆加元空间直接顶到 OOMKilled,重启循环起来比配置错误更难查。
2.2 镜像 tag 的选择:别用 latest,也别每次重启都拉
这是踩过最疼的一脚。latest配imagePullPolicy: Always,意味着每次 Pod 重建都要去仓库拉一次;仓库一抖动,Pod 就ImagePullBackOff,而滚动更新时因为 tag 没变,K8s 认为镜像没变化,新配置根本不会生效——现象是“我改了 YAML,怎么还是老行为”。
正确的做法是 tag 写死到具体版本,拉取策略用IfNotPresent,私有仓库提前把凭据建好:
# 私有仓库凭据,只需建一次 kubectl -n xxl-job create secret docker-registry registry-cred \ --docker-server=registry.example.com \ --docker-username=robot_deploy \ --docker-password='********'spec: containers: - name: admin image: registry.example.com/middleware/xxl-job-admin:<TAG> # TAG 换成你实际验证过的版本 imagePullPolicy: IfNotPresent imagePullSecrets: - name: registry-cred<TAG>这块别照抄网上的,看你下载的这套 YAML 里写的是哪个版本,就以哪个为准。admin 有官方镜像,执行器没有——执行器是你自己的业务镜像,这一点后面单独讲,很多人卡在这儿,以为执行器也能拉一个现成的。
2.3 YAML 清单拆解:五个文件各管什么、按什么顺序 apply
一套能直接跑的部署文件,通常拆成五份,按依赖顺序落地:
| 序号 | 文件 | 资源类型 | 作用 | 依赖 |
|---|---|---|---|---|
| 01 | 00-namespace.yaml | Namespace | 隔离环境 | 无 |
| 02 | 01-mysql-init.sql | SQL 脚本 | 建库建表 | MySQL 实例 |
| 03 | 02-configmap.yaml | ConfigMap | 数据源、token 等参数 | 无 |
| 04 | 03-admin.yaml | Deployment + Service | 调度中心 | 数据库、ConfigMap |
| 05 | 04-executor.yaml | Deployment + Service | 业务执行器 | admin 的 Service |
顺序错了不会立刻报错,但会浪费时间。比如先起 admin 再建库,Pod 会一直重启,你在日志里看到的是连接拒绝,得从头猜。我习惯先把 01 的 SQL 在数据库里跑完,再一次性apply -f .,最后rollout status等就绪。
2.4 数据库这一层不能省:版本、字符集和那张锁表
XXL-JOB 的元数据全在 MySQL 里,表不多但一张都不能少。MySQL 5.7 和 8.0 都能跑,差别在驱动类名和默认排序规则:5.7 用com.mysql.jdbc.Driver,8.0 必须用com.mysql.cj.jdbc.Driver,写错就是启动时ClassNotFoundException。
字符集统一到utf8mb4,排序规则显式指定utf8mb4_general_ci。8.0 默认是utf8mb4_0900_ai_ci,如果建库时用了默认值,后面和 5.7 的环境做数据迁移会撞上Illegal mix of collations,这个错在跨环境同步任务配置时特别常见。
还有一张容易被忽略的表:xxl_job_lock。调度中心多副本部署时靠它做数据库层面的分布式锁,保证同一个任务不会被两个 admin 同时触发。有人“清理冗余表”把它删了,结果就是任务重复执行,日志里还看不出异常——这张表是集群模式的后悔药,别动。
3. 从 apply 到三个 Pod 起来:XXL-JOB 的 YAML 按什么顺序落地
3.1 先建库导表,再谈部署
数据库没准备好,后面全是白搭。建库语句显式写字符集和排序规则:
CREATE DATABASE IF NOT EXISTS xxl_job DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 建一个专用账号,别用 root 跑调度中心 CREATE USER 'xxl_job'@'%' IDENTIFIED BY '********'; GRANT ALL PRIVILEGES ON xxl_job.* TO 'xxl_job'@'%'; FLUSH PRIVILEGES;然后导入随包的tables_xxl_job.sql。如果 MySQL 跑在集群内,用一次性的 Job 或直接 exec 进去导:
# 从本机把 SQL 灌进集群里的 MySQL Pod kubectl -n middleware exec -i mysql-0 -- \ mysql -uxxl_job -p'********' xxl_job < tables_xxl_job.sql # 确认关键表都在 kubectl -n middleware exec -i mysql-0 -- \ mysql -uxxl_job -p'********' -e "use xxl_job; show tables;"期望看到xxl_job_info、xxl_job_log、xxl_job_group、xxl_job_registry、xxl_job_lock这些表。少一张,admin 启动时就会在初始化那一步抛异常,日志里是一长串 MyBatis 的映射错误,看着吓人,其实回去补表就行。
3.2 admin 的 Deployment:PARAMS、探针和资源三处必须改
调度中心的镜像启动脚本支持用PARAMS环境变量透传 Spring Boot 参数,这是最省事的做法,不用自己打包 configmap 挂载。核心片段:
apiVersion: apps/v1 kind: Deployment metadata: name: xxl-job-admin namespace: xxl-job spec: replicas: 1 selector: matchLabels: app: xxl-job-admin template: metadata: labels: app: xxl-job-admin spec: containers: - name: admin image: registry.example.com/middleware/xxl-job-admin:<TAG> imagePullPolicy: IfNotPresent ports: - containerPort: 8080 env: - name: TZ value: Asia/Shanghai - name: PARAMS value: >- --spring.datasource.url=jdbc:mysql://mysql.middleware.svc.cluster.local:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true --spring.datasource.username=xxl_job --spring.datasource.password=******** --xxl.job.accessToken=******** resources: requests: cpu: 500m memory: 512Mi limits: cpu: "1" memory: 1Gi readinessProbe: httpGet: path: /xxl-job-admin/toLogin port: 8080 initialDelaySeconds: 40 periodSeconds: 10 failureThreshold: 6 livenessProbe: tcpSocket: port: 8080 initialDelaySeconds: 90 periodSeconds: 20几处细节值得说:
PARAMS里的连接串用>-折叠写法拼成一行,注意&在 YAML 里别裸写在行首,放在字符串中间没问题,但保险起见整段加引号或折叠块。
探针路径必须是/xxl-job-admin/toLogin,不是根路径/。admin 是传统 Spring MVC 应用,根路径返回 302 到登录页,有些探针配置不跟随跳转就会判定失败,Pod 永远不 Ready,Service 后面一个可用端点都没有,你以为是服务挂了,其实是探针写错了。initialDelaySeconds给到 40 秒以上,是因为首次启动要初始化数据库连接池和调度线程,太快探活会把正常启动判死。
liveness 用 tcpSocket 而不是 httpGet,是为了避免数据库短时抖动导致探针失败引发重启——重启解决不了数据库问题,只会让启动更慢。
3.3 执行器不是官方镜像:业务镜像怎么接进 YAML
这是新手最容易误解的一环。执行器没有现成镜像可拉,它是你自己的 Spring Boot 应用,引入xxl-job-core依赖后打包,再写一个配置类把执行器注册上去:
@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:9999}") private int port; @Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); executor.setAdminAddresses(adminAddresses); // 调度中心地址,多个用逗号分隔 executor.setAppname(appName); // 执行器名,要和 admin 页面里录入的一致 executor.setPort(port); // 内置 Netty 端口,默认 9999 executor.setAccessToken(accessToken); // 必须和 admin 完全一致 executor.setLogPath("/data/applogs/xxl-job/jobhandler"); // 任务日志落盘路径 executor.setLogRetentionDays(7); // 日志保留天数,按磁盘容量调 return executor; } }setAppname的值是执行器和调度中心之间的约定键:admin 页面里“执行器管理”新增的那条记录,AppName 必须和这里一字不差,大小写、横线都算。setPort对应的容器端口要一起暴露,YAML 里containerPort: 9999加上 Service 的 9999 映射,少一个,注册就上不去。
setLogPath指向的目录建议挂emptyDir或 PVC。容器重启后日志丢失,任务失败时你连执行输出都看不到,只剩一个失败状态,排查全靠猜。
执行器的 Deployment 本体和 admin 差不多,主要是PARAMS换成执行器自己的参数:
env: - name: PARAMS value: >- --xxl.job.admin.addresses=http://xxl-job-admin:8080/xxl-job-admin --xxl.job.executor.appname=xxl-job-executor-demo --xxl.job.executor.port=9999 --xxl.job.accessToken=********地址里用的是 admin 的 Service 名xxl-job-admin,同命名空间内可直接解析,不必写完整域名。跨命名空间就补上.xxl-job.svc.cluster.local。
3.4 对外暴露:ClusterIP、NodePort、Ingress 三选一
内部使用只需要 ClusterIP,业务方通过集群内域名访问。要从办公网打开管理页面,再选 NodePort 或 Ingress。Ingress 的写法在 1.22 之后必须是 v1:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: xxl-job-admin namespace: xxl-job spec: ingressClassName: nginx rules: - host: xxl-job.example.com http: paths: - path: /xxl-job-admin pathType: Prefix backend: service: name: xxl-job-admin port: number: 8080注意path要带上应用上下文/xxl-job-admin,只写/的话静态资源和接口路径会对不上,页面能打开但登录请求 404。如果必须在根路径访问,就得在 admin 启动参数里改server.servlet.context-path,但改了之后执行器注册地址也要跟着改,牵一发动全身,不如路径带前缀。
4. 配置项逐个过:数据源、accessToken、执行器地址这三处最容易写错
4.1 数据源:URL 里四个参数不能少
连接串看着简单,实际有四个参数直接影响能不能连上、连上之后行为对不对:
spring.datasource.url=jdbc:mysql://mysql.middleware.svc.cluster.local:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true spring.datasource.username=xxl_job spring.datasource.password=******** spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000serverTimezone不写,MySQL 8.0 驱动会按 UTC 解释时间,任务的下次触发时间整体偏 8 小时;useSSL=false在集群内部通常直接关掉,开着但没配证书会建立连接失败;allowPublicKeyRetrieval=true是 8.0 默认加密方式下的必需项,少了报Public Key Retrieval is not allowed;rewriteBatchedStatements=true对日志批量写入有帮助,调度中心的日志表增长很快,这个参数能让批量插入真正合并。
连接池上限别拍脑袋写大。调度中心是单实例长连接为主,20 个连接足够;设成 100 反而可能把数据库的max_connections吃满,影响同实例上的其他库。
4.2 accessToken:两端必须完全一致,空值也不行
1.9 之后版本默认启用通信校验,admin 的xxl.job.accessToken和执行器的setAccessToken必须是同一串字符。实践里有三种写法会导致注册失败:admin 设了、执行器没设;两边设了但一个有空格;用了特殊字符在PARAMS透传时被 shell 解释。
建议用纯字母数字的 32 位串,两边都从同一个配置源取。如果 admin 用 ConfigMap、执行器用环境变量,记得改的时候一起改。校验失败的表现很隐蔽:执行器日志里是 401 或者“registry fail”,admin 页面上看不到机器,任务调度时报“执行器地址为空”。
临时排查可以先把两边都置空验证链路是否通,确认后再填回去。但生产环境不要长期空着,等于把注册接口完全敞开。
4.3 执行器注册地址:自动注册在 K8s 里到底靠不靠谱
执行器默认走自动注册,向 admin 上报自己的 IP 和端口,admin 把这些地址存进xxl_job_registry表,调度时直接连。在 K8s 里这套逻辑能不能work,取决于 admin 到执行器的网络路径:
同集群同命名空间,Pod IP 直接可达,自动注册没问题。跨命名空间也通常可以,前提是没有 NetworkPolicy 拦住 9999。但如果 admin 部署在集群外,或者执行器用了hostNetwork,上报的地址就对不上了,需要在执行器侧显式指定:
xxl.job.executor.ip=10.0.0.21 xxl.job.executor.address=http://10.0.0.21:9999/address优先级高于ip+port,填了它就以它为准。手工录入模式也常用,在 admin 页面“执行器管理”里把地址写死成 Service 名或 NodePort,代价是扩缩容后要手动维护。
自动注册还有个副作用:Pod 重建后 IP 变化,旧地址会在注册表里残留约 90 秒(靠心跳超时清理)。这期间调度可能打到已经销毁的 IP 上,任务日志显示“连接超时”。滚动更新时如果流量敏感,宁可先缩到 1 副本再更新。
4.4 JVM 参数与时区:任务慢 8 小时的那个坑
时区要三处对齐:容器TZ、JVM-Duser.timezone、数据库连接串serverTimezone。缺一处,cron 表达式就可能按 UTC 解析,你配的0 0 2 * * ?实际在北京时间上午 10 点触发。
env: - name: TZ value: Asia/Shanghai - name: JAVA_OPTS value: >- -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Duser.timezone=Asia/Shanghai -Dfile.encoding=UTF-8堆给 512m 起步,调度中心的对象不多,瓶颈在数据库不在堆。-Dfile.encoding=UTF-8是为了任务参数里有中文时不出现乱码——这个错很难定位,因为任务本身执行成功了,只是参数变了样。执行器侧同样要配,尤其是日志文件里的中文输出。
5. 部署避坑:五个卡住过我的现象和它们的解法
5.1 Pod 一直 CrashLoopBackOff,日志停在数据源初始化
现象:kubectl logs只看到 Spring Boot 启动 banner 之后就没了,或者报Communications link failure。
原因:数据库不可达,或者账号权限不足。常见于数据库和 admin 不在同一命名空间但用了短域名,DNS 解析失败。
解决:先进 admin Pod 里用nc -zv mysql.middleware.svc.cluster.local 3306验证连通性;再用同样的账号密码从 Pod 内跑一次mysql -h ... -u ... -p确认权限。短域名不通就换成完整 FQDN。另外检查数据库的白名单,有些云数据库只允许特定网段。
5.2 执行器列表里显示离线,机器 registries 为空
现象:admin 页面“执行器管理”能看到执行器记录,但“OnLine 机器地址”是空的。
原因:三种可能——accessToken 不一致、9999 端口不通、AppName 拼错。
解决:先看执行器日志里有没有注册成功的记录;再在 admin Pod 里curl http://执行器IP:9999/试探端口;最后逐字符比对 AppName。三条都排除后,检查 NetworkPolicy 和 Service 的 targetPort 是否指到了 9999 而不是 8080。
5.3 任务到点没触发,调度日志里一条记录都没有
现象:任务配置看起来正常,cron 也到了,但“调度日志”列表是空的。
原因:调度中心的时间不对,或者任务状态是“停止”而不是“运行”。还有一种情况是 cron 表达式用了 Quartz 格式但写错了位数。
解决:先看 admin 容器内的date,和宿主机对一下;再确认任务状态;最后把 cron 换成0/30 * * * * ?这种 30 秒一次的做快速验证。如果日志里出现“调度成功但执行失败”,那就是执行器侧的问题,看handleCode。
5.4 ImagePullBackOff,节点上明明有镜像
现象:kubectl describe pod显示Failed to pull image,但docker images在节点上能看到同名镜像。
原因:imagePullPolicy: Always配合私有仓库凭据失效,或者 tag 是latest导致每次都拉。
解决:把策略改成IfNotPresent,tag 写死版本。凭据过期就重建 Secret,注意 Secret 必须和 Pod 同命名空间。还有一个隐蔽情况:节点打了污点或走了特定的镜像加速配置,实际拉取路径和你以为的不一样。
5.5 滚动更新后任务重复执行
现象:一次更新之后,同一个任务在同一分钟被触发两次。
原因:admin 起了两个副本,但没配数据库锁相关参数,或者xxl_job_lock表被删了。
解决:确认xxl_job_lock表存在且有数据;多副本部署时确保所有副本连的是同一个库。如果只是单副本需求,replicas保持 1,用strategy: Recreate避免新旧 Pod 并存期间重复调度。
6. 装完怎么验证跑通:从 Pod 状态到调度日志的四层自检
apply 完看到created不算数,我固定按四层往下走,每层不过就不往下看。
第一层看 Pod 状态和重启次数:
kubectl -n xxl-job get pods -o wide kubectl -n xxl-job rollout status deploy/xxl-job-admin --timeout=180sREADY 1/1、RESTARTS 0才算过。如果重启次数是 1 但当前 Running,别放过,去看上一次的日志,多半是启动初期探针太急。
第二层看 HTTP 接口能不能返回 200,绕过浏览器直接打:
kubectl -n xxl-job exec deploy/xxl-job-admin -- \ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/xxl-job-admin/toLogin返回200说明应用上下文、端口、探针路径三者一致。返回302一般是路径少了前缀,返回000是端口没起。
第三层登录页面确认执行器在线。默认账号admin,密码123456,进去第一件事是改密码,第二件事是看“执行器管理”里的 OnLine 机器数是不是等于你起的副本数。
第四层做一次端到端触发,这是唯一能证明“调度链路通”的验证:
| 检查项 | 操作 | 期望结果 |
|---|---|---|
| 执行器在线 | 执行器管理页 | OnLine 机器数 ≥ 1 |
| 任务可调度 | 新建 GLUE(Shell) 任务,内容echo hello-xxl-job | 保存成功 |
| 手动触发 | 点“执行一次” | 调度日志出现一条记录 |
| 执行回传 | 查看日志详情 | handleCode=200,执行备注含hello-xxl-job |
| 定时触发 | cron 设0/30 * * * * ?,等一分钟 | 连续出现两条调度记录 |
四层全过,这套 YAML 才算真的部署成功。任何一层卡住,回到第 5 章对号入座,基本都在那五条里。
从那以后我每次部署完 XXL-JOB,不管多急,都强制把echo hello-xxl-job这个 GLUE 任务跑一遍——页面能打开不代表调度能通,只有日志里那条handleCode=200才是真的。这套按顺序编号的 YAML 和初始化 SQL 我整理在一起了,把镜像前缀和密码换成自己的就能直接 apply,希望你一次跑通,不用在 CrashLoop 里耗一晚上。
本文还有配套的精品资源,点击获取