这是 XXL-JOB 接入 Service Mesh / K8s 后最典型的冲突点:
XXL-JOB 的执行器模型天然假设:调度中心能看到每一个执行器实例地址。
Service Mesh / K8s Service 模型天然假设:调用方只看到一个服务名,由 Service / Sidecar 做负载均衡。
所以如果 XXL-JOB Admin 只配置:
http://xxljob-executor.xxx.svc.cluster.local:9999那对 XXL-JOB 来说,10 个 Pod 会退化成 1 个执行器地址。这会影响 XXL-JOB 的路由策略、分片广播、故障转移、忙碌转移、一致性哈希等能力。
结论先说
在 Service Mesh 中跑 XXL-JOB,推荐分三种模式:
| 模式 | 适用场景 | 推荐度 |
|---|---|---|
| 模式一:单服务名 + Mesh LB | 普通定时任务,只要执行一次即可 | 可用,但能力退化 |
| 模式二:Pod 级注册到 XXL-JOB | 需要分片、广播、故障转移、实例级治理 | 推荐 |
| 模式三:调度中心只触发“任务服务”,内部自行分片/抢锁 | 企业级任务平台、复杂批处理 | 最推荐长期方向 |
1. 为什么只暴露 Service Name 会有问题?
XXL-JOB 的执行器本质是这样工作的:
XXL-JOB Admin
|
| 选择一个 executor address
v
Executor Instance执行器注册上来后,Admin 会维护一批地址:
10.1.1.11:9999
10.1.1.12:9999
10.1.1.13:9999
...然后根据路由策略选择实例:
FIRST
LAST
ROUND
RANDOM
CONSISTENT_HASH
LEAST_FREQUENTLY_USED
LEAST_RECENTLY_USED
FAILOVER
BUSYOVER
SHARDING_BROADCAST但如果进入 Service Mesh 后只剩:
xxljob-executor.default.svc.cluster.local:9999那么 Admin 看到的是:
只有 1 个地址于是:
ROUND 没意义
RANDOM 没意义
FAILOVER 没意义
BUSYOVER 没意义
SHARDING_BROADCAST 只能广播给一个“逻辑地址”
CONSISTENT_HASH 退化Mesh 后面虽然有 10 个 Pod,但这个负载均衡发生在 XXL-JOB 之后,XXL-JOB Admin 不知道真实实例是谁。
2. 模式一:单 Service Name,由 Mesh 负载均衡
架构如下:
XXL-JOB Admin
|
| http://executor-svc:9999
v
K8s Service / Envoy Sidecar
|
+--> Pod-1
+--> Pod-2
+--> Pod-3
+--> ...这种方式最简单。
优点
接入成本最低:
不用暴露 Pod IP
不用改注册逻辑
不用处理 Pod 生命周期
不用关心执行器动态上下线适合:
每天跑一次的报表任务
普通清理任务
简单同步任务
允许任意一个实例执行即可的任务缺点
XXL-JOB 的执行器实例感知能力丢失。
尤其这几个能力会失效或弱化:
分片广播
故障转移
忙碌转移
一致性哈希
执行器实例级监控
精确 kill 任务
精确查看哪个实例执行更关键的是,如果任务是 长耗时任务,Mesh 层的重试、超时、连接池策略也可能带来副作用。
例如:
XXL-JOB Admin 调 executor-svc
Envoy 选择 Pod-3
Pod-3 执行中
Admin 后续 kill / log / callback 可能路径不稳定如果没有 sticky / 实例级寻址,就可能出现行为不一致。
所以这个模式只能用于 轻量、幂等、单实例执行任务。
3. 模式二:Pod 级注册到 XXL-JOB,保留 XXL-JOB 原生调度能力
这是更合理的 Mesh/K8s 适配方式。
核心思路是:
服务治理走 Mesh,任务调度注册仍然保留 Pod 实例级地址。
架构:
+----------------+
| XXL-JOB Admin |
+----------------+
^ ^ ^
| | |
register | | | register
| | |
Pod-1 Pod-2 Pod-3 ... Pod-10每个 Pod 启动时,把自己的地址注册给 XXL-JOB Admin:
10.244.1.23:9999
10.244.2.45:9999
10.244.3.18:9999
...而不是注册:
executor-svc:9999这样 Admin 仍然可以看到 10 个真实执行器。
K8s 中怎么拿到 Pod IP?
通过 Downward API 注入:
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: XXL_JOB_EXECUTOR_IP
value: "$(POD_IP)"
- name: XXL_JOB_EXECUTOR_PORT
value: "9999"应用配置:
xxl:
job:
executor:
ip: ${POD_IP}
port: 9999或者:
xxl.job.executor.ip=${POD_IP}
xxl.job.executor.port=9999这样每个 Pod 注册到 XXL-JOB Admin 的地址就是:
PodIP:9999这种模式下 Mesh 怎么处理?
有两种做法。
方式 A:XXL-JOB 调度流量不经过 Service,直连 Pod IP
XXL-JOB Admin -> PodIP:9999这最接近 XXL-JOB 原生模型。
但前提是:
Admin 所在网络能访问 Pod IP
Pod IP 跨集群/跨节点可达
NetworkPolicy 放通
Sidecar 不拦截或正确放行该端口如果 Admin 也在同一个 K8s 集群内,通常可行。
方式 B:走 Mesh,但保持实例级寻址
例如通过:
pod-name.headless-service.namespace.svc.cluster.local:9999也就是使用 Headless Service。
apiVersion: v1
kind: Service
metadata:
name: xxl-executor-headless
spec:
clusterIP: None
selector:
app: xxl-executor
ports:
- port: 9999
targetPort: 9999然后每个 Pod 有稳定 DNS:
pod-0.xxl-executor-headless.ns.svc.cluster.local
pod-1.xxl-executor-headless.ns.svc.cluster.local
...如果是普通 Deployment,Pod 名称不稳定;如果要强实例标识,可以用 StatefulSet。
4. 模式二的问题:Pod IP 注册会带来生命周期问题
Pod IP 不是稳定资产。
Pod 重启、扩缩容、漂移之后,地址会变化:
旧地址:10.244.1.23:9999
新地址:10.244.3.91:9999所以必须确保:
Pod 启动时自动注册
Pod 停止时自动摘除
Admin 定期心跳剔除失效实例
readiness 未通过前不注册
preStop 阶段尽量反注册否则 Admin 里会看到脏地址。
建议处理方式:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 20"]让 Pod 有时间完成:
停止接任务
回调完成
注销执行器
等待 Mesh / Endpoint 摘除另外要配置:
terminationGracePeriodSeconds: 60同时任务本身要支持幂等和重入保护。
5. 分片广播场景必须用 Pod 级注册
如果你用了 XXL-JOB 的:
SHARDING_BROADCAST例如 10 个 Pod 分别处理:
Pod-0: shardIndex=0, shardTotal=10
Pod-1: shardIndex=1, shardTotal=10
...
Pod-9: shardIndex=9, shardTotal=10那么 Admin 必须看到 10 个执行器地址。
如果只看到一个 Service Name:
executor-svc:9999那 Admin 认为:
shardTotal = 1
shardIndex = 0Mesh 后面的 10 个 Pod 不会变成 XXL-JOB 语义里的 10 个分片。
所以:
只要依赖 XXL-JOB 分片广播,就不能只注册 K8s Service Name。
6. 企业级推荐:调度中心只做触发,任务服务内部做分布式执行
长期看,我更推荐把 XXL-JOB 的定位收敛成:
触发器 / 调度入口而不是让它负责所有分片、实例路由、分布式执行语义。
更企业级的架构是:
XXL-JOB Admin
|
| trigger job
v
Job Orchestrator / Task Service
|
+--> 创建任务实例
+--> 写入任务表
+--> 分片
+--> 抢锁
+--> 幂等
+--> 状态机
+--> 重试
+--> 补偿
+--> 进度追踪
+--> 结果归档再由任务服务内部调度 Worker:
Task Service
|
+--> Worker Pod-1
+--> Worker Pod-2
+--> Worker Pod-3
+--> ...任务分片不依赖 XXL-JOB 实例路由,而是依赖:
DB 分片表
Redis 锁
Consul Lock
ZK
MySQL 乐观锁
Kafka 分区
Pulsar Topic
K8s Job
Argo Workflow这种模式下,XXL-JOB 只需要调用一个服务名:
job-orchestrator-svc后面的 10 个 Pod 是否可见并不重要。
7. 推荐落地架构
结合你们已有的环境:K8s、Consul、Service Mesh、XXL-JOB、Apollo、网关、可观测,我建议按任务类型分层治理。
A. 普通任务
例如:
缓存清理
日报生成
轻量同步
状态巡检
简单补偿可以:
XXL-JOB -> executor service name -> Mesh LB -> 任意 Pod要求:
任务幂等
不依赖分片
不依赖指定实例
允许重复触发保护B. 分片任务
例如:
批量订单扫描
库存同步
会员积分补偿
账务对账
消息重推建议:
每个 Pod 注册自身 PodIP:port 到 XXL-JOB
XXL-JOB 使用 SHARDING_BROADCAST要求:
Pod 级注册
心跳剔除
preStop 优雅下线
任务幂等
分片边界稳定C. 核心批处理 / 长任务
例如:
财务对账
履约补偿
库存重算
价格重算
会员积分重算
大促数据修复建议:
XXL-JOB 只触发 Task Service
Task Service 内部负责任务实例、分片、状态、重试、补偿这是最稳的。
8. 对你这个问题的直接答案
你问:
执行器是一个 10 台 Pods 的群集,但只有一个服务名;对 XXLJOB 来说 10 台机器就不可见了,怎么办?
答案是:
不能只用普通 Service Name 承载 XXL-JOB 的执行器注册,否则 XXL-JOB 的实例级调度能力会退化。
你有三个选择:
1. 接受退化:只注册 service name,任务由 Mesh 随机打到某个 Pod
2. 保留能力:每个 Pod 用 Pod IP / Headless DNS 注册到 XXL-JOB
3. 架构升级:XXL-JOB 只负责触发,真正分布式执行由任务服务自己完成我建议:
默认用 1
需要分片广播/故障转移/实例感知时用 2
核心批处理/企业级任务平台用 39. 推荐标准
可以定一个企业内部规范:
XXL-JOB 接入 K8s / Mesh 规范任务分类
L1: Trigger Job
- 单次触发
- 不关心执行实例
- 可走 Service Name
L2: Sharding Job
- 依赖 XXL-JOB 分片广播
- 必须 Pod 级注册
L3: Orchestrated Job
- 长任务 / 核心任务 / 可恢复任务
- XXL-JOB 只触发任务服务
- 任务服务内部编排注册规范
普通任务:
executor.address = http://executor-svc:9999
分片任务:
executor.ip = ${POD_IP}
executor.port = 9999
强稳定分片任务:
StatefulSet + Headless Service + stable identityMesh 规范
调度请求端口独立
禁用不安全重试
设置合理 timeout
保留 traceId / jobId / logId
sidecar 放通 callback
Pod 终止前优雅下线10. 一句话总结
XXL-JOB 的调度模型是“执行器实例可见”,Service Mesh 的模型是“服务实例屏蔽”。两者直接叠加会让 XXL-JOB 失去实例级调度能力。普通任务可以接受,分片任务必须 Pod 级注册,核心批处理最好让 XXL-JOB 只做触发,由任务服务内部完成分片、锁、状态机和补偿。