XXL-JOB 在 Consul Service Mesh 中的分片调度方案
摘要
问题背景是XXL-JOB 的分片广播依赖调度中心能看到多个执行器实例地址;如果执行器只注册一个 Kubernetes Service 或 Mesh 服务名,XXL-JOB Admin 只能看到一个逻辑地址,SHARDING_BROADCAST、FAILOVER、BUSYOVER、ROUND、RANDOM、CONSISTENT_HASH 等实例级路由能力都会退化。
方案收敛为三种模式:普通任务走 ServiceName;XXL-JOB 原生分片走 Pod 级注册;核心批处理走独立任务编排服务。
官方依据
- XXL-JOB 官方文档说明:执行器支持自动注册,调度中心可以发现注册任务并触发执行;执行器集群部署时支持多种路由策略,包括分片广播;分片广播会对执行器集群中的实例广播执行,并基于分片参数开发任务逻辑。参考:XXL-JOB 官方文档 与 English Documentation。
- XXL-JOB 执行器配置中,
xxl.job.executor.address为空时使用内嵌服务IP:PORT注册;不为空时优先使用该配置作为注册地址。参考:执行器配置说明。 - Consul 官方文档说明:Consul Service Mesh 使用基于 SPIFFE X.509 标准的证书做服务身份和 mTLS;透明代理会把 Pod 的入站/出站流量重定向到 sidecar proxy;在 Kubernetes 中启用透明代理需要
consul.hashicorp.com/connect-inject与consul.hashicorp.com/transparent-proxy等配置,并通过 Service Intentions 显式允许服务间通信。参考:Consul service mesh、Enable transparent proxy mode on Kubernetes、Consul on Kubernetes annotations。
核心问题
XXL-JOB 的调度模型是:
XXL-JOB Admin
-> 选择一个或多个 executor address
-> 调用对应 Executor 实例当执行器集群有 10 个 Pod 时,XXL-JOB 理想上应该看到 10 个地址:
10.244.1.21:9999
10.244.1.22:9999
10.244.1.23:9999
...如果只注册一个服务名:
http://xxl-job-executor.default.svc.cluster.local:9999XXL-JOB Admin 只会看到一个地址。Consul Envoy 或 Kubernetes Service 后面即使有 10 个 Pod,也只是网络层负载均衡,不会变成 XXL-JOB 语义里的 10 个分片。
因此分片广播下会退化为:
shardTotal = 1
shardIndex = 0这就是 XXL-JOB 与 Service Mesh 默认抽象之间的冲突:XXL-JOB 要实例列表,Mesh 倾向隐藏实例列表。
模式一:ServiceName + Mesh 负载均衡
适用于普通定时任务,只要求任意一个实例执行一次。
XXL-JOB Admin
-> xxl-job-executor.service.consul 或 Kubernetes Service
-> Consul Envoy / K8s Service
-> 任意 Executor Pod执行器注册地址:
xxl.job.executor.address=http://xxl-job-executor.default.svc.cluster.local:9999适用任务:
- 缓存清理
- 轻量同步
- 日报生成
- 巡检任务
- 允许任意一个实例执行的补偿任务
约束:
- 任务必须幂等。
- 不依赖分片广播。
- 不依赖指定实例。
- 不依赖 XXL-JOB 的实例级失败转移或忙碌转移。
- Mesh 层重试、超时、连接池策略不能改变任务语义。
这个模式最简单,但不适合真正的分片任务。
模式二:Pod 级注册,保留 XXL-JOB 原生分片
适用于继续使用 XXL-JOB 的 SHARDING_BROADCAST、实例级故障转移、忙碌转移、按实例查看日志等能力。
核心原则:
服务治理可以走 Mesh
任务调度注册必须保留实例级地址推荐让每个执行器 Pod 注册自己的 PodIP:port:
xxl.job.executor.appname=xxl-job-executor-mesh
xxl.job.executor.ip=${POD_IP}
xxl.job.executor.port=9999
xxl.job.executor.address=注意:xxl.job.executor.address 必须保持为空,或者明确填入该 Pod 自己的可达地址。不要在分片模式下把它设置为普通 ServiceName。
Deployment 示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: xxl-job-executor
spec:
replicas: 3
selector:
matchLabels:
app: xxl-job-executor
template:
metadata:
labels:
app: xxl-job-executor
annotations:
consul.hashicorp.com/connect-inject: "true"
consul.hashicorp.com/transparent-proxy: "true"
consul.hashicorp.com/connect-service: "xxl-job-executor"
consul.hashicorp.com/connect-service-port: "9999"
spec:
containers:
- name: executor
image: your-executor-image:latest
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: XXL_JOB_EXECUTOR_IP
value: "$(POD_IP)"
- name: XXL_JOB_EXECUTOR_PORT
value: "9999"
ports:
- name: job
containerPort: 9999
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 20"]
terminationGracePeriodSeconds: 60Spring Boot 配置:
xxl:
job:
admin:
addresses: http://xxl-job-admin.default.svc.cluster.local:8080/xxl-job-admin
executor:
appname: xxl-job-executor-mesh
ip: ${XXL_JOB_EXECUTOR_IP:}
port: ${XXL_JOB_EXECUTOR_PORT:9999}
address:Consul Intentions
如果 Consul Mesh 开启默认拒绝或严格 ACL,需要显式允许 Admin 调用 Executor:
apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceIntentions
metadata:
name: xxl-job-executor
spec:
destination:
name: xxl-job-executor
sources:
- name: xxl-job-admin
action: allowServiceDefaults
XXL-JOB executor 的调度端口可以按 TCP 治理,避免 L7 HTTP 路由、重试、重写影响 XXL-JOB 自己的失败判断:
apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceDefaults
metadata:
name: xxl-job-executor
spec:
protocol: tcp如果团队明确需要 HTTP L7 可观测或 L7 intentions,再改为 http,但要同步确认 Consul 的重试、超时、路由规则不会破坏任务执行语义。
关于 Pod IP 直连与 mTLS
原草稿中“Pod IP 直连也能完美享受 mTLS”的判断需要加前提。更准确的说法是:
当 Admin 与 Executor 都被 Consul Connect 注入、透明代理生效、执行器端口被 Consul service 正确注册、Intentions 允许访问、且流量没有被排除出 transparent proxy 时,应用层访问
PodIP:9999可以被本地 sidecar 拦截并转换为 Envoy 间 mTLS 流量。
mTLS 证书绑定的是服务身份,不是 Pod IP 本身。Consul 使用 SPIFFE X.509 证书表达服务身份,Envoy 之间完成双向证书校验和加密连接,Intentions 再按源服务与目标服务授权。
因此不能只凭“代码里访问的是 Pod IP”判断是否有 mTLS。必须验证流量是否真正经过 sidecar。
需要检查:
- Pod 上存在
consul-dataplane或 Envoy sidecar。 - Pod 注解包含
consul.hashicorp.com/connect-inject: "true"。 - transparent proxy 对该 Pod 生效。
- 没有用
transparent-proxy-exclude-inbound-ports或transparent-proxy-exclude-outbound-ports排除9999。 xxl-job-admin到xxl-job-executor的 Intentions 是 allow。- NetworkPolicy 允许 sidecar 和应用端口所需通信。
验证命令示例:
kubectl get pod <admin-pod> -o jsonpath='{.metadata.annotations}'
kubectl get pod <executor-pod> -o jsonpath='{.metadata.annotations}'
kubectl logs <admin-pod> -c consul-dataplane
kubectl logs <executor-pod> -c consul-dataplane
kubectl exec <admin-pod> -c consul-dataplane -- curl -s http://127.0.0.1:19000/clusters如果平台开放 Envoy admin endpoint,可以在 clusters 或 listeners 中确认执行器服务、端点和 TLS transport socket。不同 Consul/Envoy 部署的 admin 端口可能不同,不能把 127.0.0.1:19000 视为固定值。
Headless Service 方案
Headless Service 可以用于实例级 DNS 发现:
apiVersion: v1
kind: Service
metadata:
name: xxl-job-executor-headless
spec:
clusterIP: None
selector:
app: xxl-job-executor
ports:
- name: job
port: 9999
targetPort: 9999但要注意:
- 普通 Deployment 的 Pod 名称不稳定,不适合作为长期实例身份。
- Headless Service 返回多个 endpoint,并不代表 XXL-JOB Admin 会自动把这些 endpoint 当作多个执行器地址。
- 如果要让 XXL-JOB 看到多个地址,仍然需要执行器逐个注册,或额外实现 endpoint 同步器,把 endpoint 列表同步到 XXL-JOB 执行器地址列表。
- 如果要稳定实例名,可以使用 StatefulSet,让地址类似
xxl-job-executor-0.xxl-job-executor-headless.default.svc.cluster.local:9999。
对 XXL-JOB 原生分片来说,最直接稳定的方式仍是执行器 Pod 自注册。
生命周期与脏地址治理
Pod 级注册的风险在生命周期:
Pod 重启、扩缩容、节点漂移
-> Pod IP 变化
-> Admin 可能短时间保留旧地址
-> 分片广播可能触发失败、重试或重复执行治理要求:
- readiness 通过前不要对外接任务。
- 停止前给执行中任务留出收尾时间。
preStop与terminationGracePeriodSeconds覆盖最长安全下线窗口。- 依赖 XXL-JOB 心跳剔除失效实例,同时监控注册表中旧地址滞留时间。
- 任务必须幂等,按
jobId + triggerTime + shardIndex或业务唯一键做重复保护。 - 长任务需要 checkpoint,不能只依赖进程内状态。
建议在任务处理逻辑里记录:
job_id
trigger_time
shard_index
shard_total
executor_address
attempt
business_range
status这样才能排查滚动升级、扩缩容和重试导致的重复或缺口。
模式三:XXL-JOB 只做触发,任务服务内部编排
对核心批处理、长任务、财务对账、库存重算、履约补偿等任务,不建议把全部分布式执行语义压在 XXL-JOB 的实例路由上。
更稳的长期架构是:
XXL-JOB Admin
-> job-orchestrator service
-> 创建任务实例
-> 写任务表和分片表
-> Worker 抢占分片
-> 执行、checkpoint、重试、补偿、归档任务分片可以由这些机制承担:
- 数据库分片表 + 乐观锁
- Redis/Consul lock
- Kafka/Pulsar 分区
- Kubernetes Job
- Argo Workflows
- 内部任务状态机
这种模式下,XXL-JOB 只需要调用一个服务名。Mesh 隐藏后端实例不是问题,因为真正的分片、锁、状态、重试都在任务服务内部完成。
推荐标准
| 任务等级 | 调度方式 | 注册方式 | 适用场景 |
|---|---|---|---|
| L1 Trigger Job | XXL-JOB 触发一次 | ServiceName | 普通巡检、缓存清理、轻量同步 |
| L2 Sharding Job | XXL-JOB 分片广播 | Pod 级注册 | 批量扫描、消息重推、可并行补偿 |
| L3 Orchestrated Job | XXL-JOB 只触发任务服务 | ServiceName | 核心长任务、可恢复批处理、财务/库存/履约类任务 |
默认选择:
- 普通任务:ServiceName + Mesh LB。
- 依赖 XXL-JOB 原生分片:PodIP 或稳定实例 DNS 注册。
- 核心批处理:独立 Task Service 编排。
验收清单
分片模式上线前至少验证:
- XXL-JOB Admin 的执行器列表中能看到多个不同的 executor address。
- 新建任务选择
SHARDING_BROADCAST后,每个执行器 Pod 都能收到一次调度。 - 每个 Pod 日志能打印不同的
shardIndex,且shardTotal等于当前可用执行器数。 - 扩容后,下一轮调度能看到新的
shardTotal。 - 滚动升级期间,旧 Pod 不再接收新任务,正在执行的任务能完成或被幂等重试。
- kill、log、callback 行为能命中正确实例,或已接受其限制。
- Consul sidecar 日志和 Envoy 配置能证明流量经过 Mesh。
- Intentions、NetworkPolicy、readiness、preStop、terminationGracePeriodSeconds 均已配置。
- 任务侧有重复触发保护和失败补偿策略。
最终判断
- 不需要分片的任务,注册 ServiceName,接受 XXL-JOB 实例级能力退化。
- 需要 XXL-JOB 原生分片广播的任务,执行器必须以 Pod 级地址或稳定实例地址注册,让 Admin 看到多个 executor address。
- 核心批处理和长任务,不把 XXL-JOB 当分布式执行引擎,而是让它只触发任务编排服务。
- Consul Mesh 下的 Pod IP 直连 mTLS 必须实测确认,不能用“访问 Pod IP”或“注入 sidecar”单点推断。