主题
通信协议与路由策略
一句话:Admin 和 Executor 之间是双向 HTTP + JSON:Admin 用
/run把触发参数送到执行器,Executor 用/api/callback把结束结果送回;传输用HttpURLConnection、默认 3 秒超时,回调只更新日志状态,重试回调不会重跑 Handler。
适用版本:XXL-JOB 2.3.x / 2.4.0;抓包与 curl 示例在 Linux/macOS 上执行。
为什么需要
调度和执行拆成两个进程后,必须解决三件事:Admin 怎么把任务送过去、Executor 怎么把结果送回来、网络不通时怎么排障。
用 HTTP 的好处是两边都能独立部署、独立排障:Admin 发完请求就释放连接,不需要维持长连接;Executor 只要暴露一个端口;出问题可以直接 curl。代价是没有服务端推送,结果必须由 Executor 反过来再发一个请求。
核心机制
两端各有哪些端点
| 方向 | 端点 | 用途 |
|---|---|---|
| Admin → Executor | POST /run | 下发一次触发 |
| Admin → Executor | POST /kill | 终止某个 jobId 的线程 |
| Admin → Executor | POST /log、/logDetail | 查看执行器侧日志 |
| Admin → Executor | POST /beat | 探活,返回成功即进程可响应 |
| Admin → Executor | POST /idleBeat | 带 jobId,判断该任务线程是否空闲 |
| Executor → Admin | POST /api/registry | 注册与心跳(每 30 秒) |
| Executor → Admin | POST /api/callback | 上报执行结果;正常停机还会调 /api/registryRemove 摘除自己 |
请求体都是 JSON;配置了 accessToken 时带 XXL-JOB-ACCESS-TOKEN 头。logDateTime 等其余字段见对应版本 TriggerParam。
/run:只负责送达,不负责执行完
请求体的关键字段(2.4.0):
| 字段 | 含义 |
|---|---|
jobId | 任务 ID,Executor 用它找 JobThread |
executorHandler | Handler 名称,对应 @XxlJob("...") |
executorParams | 任务参数,XxlJobHelper.getJobParam() 读取 |
executorBlockStrategy | SERIAL_EXECUTION / DISCARD_LATER / COVER_EARLY |
executorTimeout | 执行超时秒数,0 表示不限 |
logId | Admin 侧日志主键,回调时原样带回 |
glueType / glueSource | GLUE 模式代码;BEAN 模式为空 |
broadcastIndex / broadcastTotal | 分片下标与总分片数 |
Executor 收到后把触发放进该 jobId 的队列,就返回 {"code":200}。这个 200 只代表「收下了」,不代表 Handler 执行成功。阻塞策略如果丢弃本次,/run 会返回 500,Admin 记 trigger_code=500。
/api/callback:只更新日志,不重跑 Handler
Executor 在 Handler 结束时发一次回调,核心字段是 logId、handleCode、handleMsg。Admin 按 logId 更新 xxl_job_log 的 handle_time、handle_code、handle_msg。
- 同一个
logId的重复回调会被 Admin 识别并拒绝(log repeate callback),不会重新执行 Handler。 - 执行失败后的「失败重试」是另一条链路:
executor_fail_retry_count > 0时,Admin 以RETRY类型再触发一次,产生新的logId。 - 回调失败不会导致 Handler 重跑,只会让日志状态停在
handle_code=0,直到结果丢失检查兜底。
Executor 侧的回调不是「发一次就丢」:TriggerCallbackThread 先把结果放队列,批量发往所有 Admin 地址,任意一个成功就停止;全部失败时序列化到本地 callbacklog/xxl-job-callback-*.log,另一个线程每 30 秒重试。重试的仍然只是「上报结果」。
/beat 与 /idleBeat 的区别
/beat:只证明 Executor 的 HTTP Server 能响应。进程活着、网络通,就返回成功。/idleBeat:请求体带jobId,Executor 检查该 jobId 的JobThread是否正在跑或队列非空;空闲才返回成功。
FAILOVER 路由用 /beat 按顺序找第一个存活地址;BUSYOVER 用 /idleBeat 找第一个空闲地址。两者都是「先探测再选」,不是「先发失败再换一个」。
传输细节:没有连接池
2.x 的 XxlJobRemotingUtil.postBody 直接用 java.net.HttpURLConnection,设置 JSON 头、Keep-Alive 和 token 头;连接超时与读取超时共用一个值,单位秒。2.4.0 的 ExecutorBizClient 默认 3 秒,取值不在 1–10 秒时回落到 3 秒;不同小版本是否开放配置项以官方文档为准。Executor 侧不是 Spring MVC,而是 EmbedServer 启动的 Netty HTTP 服务,默认端口 9999。
3 秒限制的是「Executor 接收并返回」的时间,不是 Handler 执行时间。Handler 跑 10 分钟,/run 也会在毫秒级返回;结果靠 /callback 另行上报。
路由策略怎么选
完整列表见 03-调度核心机制。选择逻辑:无状态用 ROUND/RANDOM;要固定落同一实例用 CONSISTENT_HASH;要避开挂掉的实例用 FAILOVER(多一次 /beat 探测);长任务要避开忙碌实例用 BUSYOVER(多一次 /idleBeat 探测);数据并行用 SHARDING_BROADCAST。FIRST/LAST 适合单实例或明确指定机器的场景,故障时不会自动切换。
最小上手
先确认 Executor 的端口和 token:
bash
# 探活:返回 {"code":200,...} 说明 HTTP Server 正常
curl -s -X POST 'http://127.0.0.1:9999/beat' \
-H 'XXL-JOB-ACCESS-TOKEN: default_token'
# 判断 jobId=1 是否空闲
curl -s -X POST 'http://127.0.0.1:9999/idleBeat' \
-H 'Content-Type: application/json' \
-H 'XXL-JOB-ACCESS-TOKEN: default_token' \
-d '{"jobId":1}'直接调 /run(用一个不存在的 logId,避免污染真实日志):
bash
curl -s -X POST 'http://127.0.0.1:9999/run' \
-H 'Content-Type: application/json' \
-H 'XXL-JOB-ACCESS-TOKEN: default_token' \
-d '{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "from-curl",
"executorBlockStrategy": "SERIAL_EXECUTION",
"executorTimeout": 0,
"logId": 900001,
"logDateTime": 1730000000000,
"glueType": "BEAN",
"glueSource": "",
"glueUpdatetime": 0,
"broadcastIndex": 0,
"broadcastTotal": 1
}'预期返回 {"code":200,"msg":null}。验证方式:
- Executor 日志里出现该 jobId 的执行记录和
param=from-curl。 curl -v看请求/响应头和状态码;或在服务器上tcpdump -i any -A -s0 port 9999看明文 JSON。ss -tnp | grep 9999看连接是否建立。Admin 的调度日志不会新增记录,因为这个logId不是 Admin 创建的,回调找不到对应行会被忽略;这是直接调/run的预期行为。
生产动作
| 配置/行为 | 默认值(2.4.0) | 说明 |
|---|---|---|
xxl.job.accessToken | 空 | 两端必须一致;为空时不做 token 校验 |
/run 连接/读取超时 | 3 秒 | ExecutorBizClient 限制在 1–10 秒,版本间可能有配置入口 |
| 注册/心跳 | 30 秒 | /api/registry |
| 回调重试 | 30 秒 | 本地 callbacklog/ 文件 |
网络方向:Executor 要能访问 Admin 的 /api/registry、/api/callback;Admin 要能访问 Executor 的 /run、/beat、/idleBeat、/kill、/log。只开一个方向,会出现「执行器在线但调度连接超时」或「任务执行了但状态不回传」。Executor 默认自动取本机 IP,容器、多网卡、NAT 环境下可能注册错;用 xxl.job.executor.ip 指定 IP,或用 xxl.job.executor.address 指定完整地址(如 http://10.0.0.12:9999/)。
排障对照:
| 现象 | 证据 | 判断 |
|---|---|---|
connect timed out | Admin 日志里的目标地址 | 网络/防火墙/端口不通,或注册地址不可达 |
read timed out | /run 超过 3 秒没有响应 | Executor 线程池或 Netty 处理阻塞;Admin 会记 trigger_code=500,但 Executor 可能已经执行,业务要幂等 |
| 回调 401 | Admin 返回 token 错误 | 两端 accessToken 不一致 |
状态长期 handle_code=0 | Executor 有成功日志、Admin 无回调 | 查 callbacklog/ 是否堆积、Admin 是否在回调窗口重启、网络是否单向不通 |
callbacklog/ 目录默认在 xxl.job.executor.logpath 下。文件长期堆积说明回调一直失败;不要直接删文件,先恢复 Admin 连通性,让重试线程把结果送出去。
边界
- HTTP 不是 RPC 框架:没有连接池、没有多路复用、没有服务端流;调度 QPS 不高时够用,高频短任务不合适。默认还是明文传输,token 只是请求头里的凭证,不是加密;跨公网要加 TLS。
- 回调不是可靠消息:本地文件重试能扛 Admin 短暂不可用,但 Executor 磁盘坏、进程被杀、文件被清理时仍会丢。
/run的 200 不代表成功:它只代表触发被接收。判断执行结果必须看/api/callback写回的handle_code。- 直接调
/run绕过 Admin:不走路由、不写 Admin 日志、不触发失败重试,只适合连通性和 Handler 调试。
讲清楚
Admin 和 Executor 之间是两条独立的 HTTP 链路:Admin 用
/run把 jobId、handler、参数、logId 和分片信息送过去,Executor 收下就返回 200;执行结束后,Executor 用/api/callback把同一个 logId 和结果送回来。/run的 200 只表示送达,执行结果只看回调。底层是HttpURLConnection,连接和读取超时共用一个值,2.4.0 默认 3 秒,没有连接池;Executor 侧是内嵌 Netty。回调失败会在本地callbacklog/落盘并每 30 秒重试,但重试只补状态,不会重跑 Handler;执行失败后的重试是另一条链路,会产生新的 logId。路由策略决定选哪个地址:FAILOVER用/beat探活,BUSYOVER用/idleBeat探空闲,分片广播则给每个实例各发一次/run。部署时前后两个方向都要放通,否则会出现「在线但调不通」或「跑了但状态不回」。适用 XXL-JOB 2.3.x / 2.4.0 | 初稿 2026-10