跳到正文

通信协议与路由策略 ​

一句话: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 → ExecutorPOST /run下发一次触发
Admin → ExecutorPOST /kill终止某个 jobId 的线程
Admin → ExecutorPOST /log、/logDetail查看执行器侧日志
Admin → ExecutorPOST /beat探活,返回成功即进程可响应
Admin → ExecutorPOST /idleBeat带 jobId,判断该任务线程是否空闲
Executor → AdminPOST /api/registry注册与心跳(每 30 秒)
Executor → AdminPOST /api/callback上报执行结果;正常停机还会调 /api/registryRemove 摘除自己

请求体都是 JSON;配置了 accessToken 时带 XXL-JOB-ACCESS-TOKEN 头。logDateTime 等其余字段见对应版本 TriggerParam。

/run:只负责送达,不负责执行完 ​

请求体的关键字段(2.4.0):

字段含义
jobId任务 ID,Executor 用它找 JobThread
executorHandlerHandler 名称,对应 @XxlJob("...")
executorParams任务参数,XxlJobHelper.getJobParam() 读取
executorBlockStrategySERIAL_EXECUTION / DISCARD_LATER / COVER_EARLY
executorTimeout执行超时秒数,0 表示不限
logIdAdmin 侧日志主键,回调时原样带回
glueType / glueSourceGLUE 模式代码;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}。验证方式:

  1. Executor 日志里出现该 jobId 的执行记录和 param=from-curl。
  2. curl -v 看请求/响应头和状态码;或在服务器上 tcpdump -i any -A -s0 port 9999 看明文 JSON。
  3. 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 outAdmin 日志里的目标地址网络/防火墙/端口不通,或注册地址不可达
read timed out/run 超过 3 秒没有响应Executor 线程池或 Netty 处理阻塞;Admin 会记 trigger_code=500,但 Executor 可能已经执行,业务要幂等
回调 401Admin 返回 token 错误两端 accessToken 不一致
状态长期 handle_code=0Executor 有成功日志、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

把一门技术讲透到既懂也会用