这是 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 = 0

Mesh 后面的 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
核心批处理/企业级任务平台用 3

9. 推荐标准

可以定一个企业内部规范:

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 identity

Mesh 规范

调度请求端口独立
禁用不安全重试
设置合理 timeout
保留 traceId / jobId / logId
sidecar 放通 callback
Pod 终止前优雅下线

10. 一句话总结

XXL-JOB 的调度模型是“执行器实例可见”,Service Mesh 的模型是“服务实例屏蔽”。两者直接叠加会让 XXL-JOB 失去实例级调度能力。普通任务可以接受,分片任务必须 Pod 级注册,核心批处理最好让 XXL-JOB 只做触发,由任务服务内部完成分片、锁、状态机和补偿。