执行器注册与路由策略
This content is not available in your language yet.
时间轮吐出一个 jobId 之后,下一个问题是「发给谁」。XXL-JOB 把注册中心直接做进了 MySQL:一张 xxl_job_registry 表 + 30 秒心跳 + 90 秒判死,再配九种路由策略挑一个地址。这一篇的重点是哪些路由策略在调度中心集群部署时会退化——这是它最容易踩、也最少被文章提到的坑。
先给答案:路由策略解决的是“任务该交给谁”,不是“任务能不能成功”
Section titled “先给答案:路由策略解决的是“任务该交给谁”,不是“任务能不能成功””执行器注册表只提供候选节点,真正的路由还要结合负载、分片、故障和任务语义。随机、轮询、故障转移、忙碌转移和分片广播,分别对应不同的优先级:平均分布、尽快成功、避开故障、避开拥塞,或让所有节点共同参与。
因此路由策略不能脱离任务语义单独评价。一个需要本地缓存的任务不能随意故障转移;一个必须全量处理数据的任务不能误用单点路由;一个追求吞吐的任务也不一定适合“永远选空闲节点”。选择策略前先判断任务是否幂等、是否有状态、是否允许重复执行,再谈负载均衡。
执行器是无状态的业务应用,会随时扩缩容。调度中心必须知道:
- 现在有哪些执行器活着(发现)
- 某个
appname对应哪几个地址(分组) - 这次触发该挑哪一个(路由)
用 Nacos/Eureka 能解决前两个,但会给「开箱即用」的定位增加一个中间件。XXL-JOB 的选择是:注册表就是一张表,心跳就是一条 upsert,判死就是一条带时间条件的 DELETE。
【执行器】registryThread(CyclicThread, 30s) │ RegistryRequest("EXECUTOR", appname, "http://ip:port/") ├──POST /api/registry──────────────────────────────────┐ │ 遍历 adminBizList,任一成功即 break │ │ ▼ │ 【调度中心】OpenApiController#api │ ├ 校验 accessToken + appname(查本地缓存) │ └ AdminBizImpl#registry │ └ JobRegistryHelper#registry │ └ 丢进 registryOrRemoveThreadPool(异步) │ └ INSERT ... ON DUPLICATE KEY UPDATE update_time │ │ │ ▼ │ ┌──── xxl_job_registry ────┐ │ │ EXECUTOR | app | addr | t │ │ └───────────────────────────┘ │ ▲ └── 停机时 POST /api/registryRemove ──► DELETE 该行 │ │ registryMonitorThread(CyclicThread, 30s)───────────────────────────────┘ ① DELETE FROM xxl_job_registry WHERE update_time < now - 90s ② SELECT 全部存活行 → 按 appname 聚合成 List<address> → 排序 → join(",") ③ UPDATE xxl_job_group SET address_list = ... (仅 address_type = 0 的组) ④ 重建 appname2GroupCache / id2GroupCache 本地缓存1. 执行器侧:一条 30 秒心跳线程
Section titled “1. 执行器侧:一条 30 秒心跳线程”registryThread = new CyclicThread("ExecutorRegistryThread#registryThread", true, () -> { RegistryRequest registryParam = new RegistryRequest( RegistTypeEnum.EXECUTOR.name(), xxlJobExecutor.getAppname(), xxlJobExecutor.getAddress()); for (AdminBiz adminBiz : xxlJobExecutor.getAdminBizList()) { Response<String> registryResult = adminBiz.registry(registryParam); if (registryResult != null && registryResult.isSuccess()) { break; // 任一成功即停 } }}, Const.BEAT_TIMEOUT * 1000L, true); // ExecutorRegistryThreadHelper.java:48-69| 常量 | 值 | 位置 |
|---|---|---|
BEAT_TIMEOUT |
30 秒 | Const.java:26 |
DEAD_TIMEOUT |
BEAT_TIMEOUT * 3 = 90 秒 |
Const.java:31 |
「任一成功即 break」是关键语义(:58):调度中心集群配了 3 个地址时,心跳只会打到第一个可达的节点上。这没问题——因为注册表在共享的 MySQL 里,任何一个节点写入,全集群都能看到。调度中心节点之间不需要同步注册信息,这是把状态外置到 DB 的直接收益。
上报的地址在 XxlJobExecutor#startEmbedServer 里确定(XxlJobExecutor.java:271-279):
this.port = this.port > 0 ? this.port : IPTool.getAvailablePort(9999);this.ip = isNotBlank(this.ip) ? this.ip : IPTool.getIp();if (isBlank(this.address)) { this.address = "http://{ip_port}/".replace("{ip_port}", IPTool.toAddressString(ip, port));}三层兜底:显式配 address → 显式配 ip:port → 自动探测网卡 IP + 从 9999 起找空闲端口。容器化部署时前两层几乎必须显式配,否则探测到的会是容器内网 IP,调度中心根本连不上。
2. 调度中心侧:异步 upsert
Section titled “2. 调度中心侧:异步 upsert”JobRegistryHelper#registry 校验完三个字段非空后,直接把 DB 操作丢进线程池就返回成功(JobRegistryHelper.java:164-194):
registryOrRemoveThreadPool.execute(() -> { int ret = ...getXxlJobRegistryMapper().registrySaveOrUpdate( registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date()); // :178 if (ret == 1) { freshGroupRegistryInfo(registryParam); } // :179-182});return Response.ofSuccess(); // :193SQL 靠唯一索引做幂等(XxlJobRegistryMapper.xml:42-47):
INSERT INTO xxl_job_registry(registry_group, registry_key, registry_value, update_time)VALUES(#{registryGroup}, #{registryKey}, #{registryValue}, #{updateTime})ON DUPLICATE KEY UPDATE update_time = #{updateTime}依赖表上的 UNIQUE KEY i_g_k_v (registry_group, registry_key, registry_value)(doc/db/tables_xxl_job.sql:24-33)。一条 SQL 同时表达「首次注册」和「续约」,比先 UPDATE 再 INSERT 的两段式少一次往返、也没有并发竞争——旧实现就在同一个 XML 文件的注释里(:49-58),可以直接对照。
线程池的拒绝策略是 r.run()(JobRegistryHelper.java:66):注册请求不能丢,池满就让 Netty 的业务线程自己跑。
freshGroupRegistryInfo是个空方法(:223-225),方法体只有一行注释// Under consideration, prevent affecting core tables。也就是说新执行器注册后,地址不会立刻出现在xxl_job_group.address_list里,必须等registryMonitorThread的下一轮(最长 30 秒)。这是新节点扩容后「为什么还没流量」的直接答案。
3. 监控线程:判死 + 聚合 + 刷缓存
Section titled “3. 监控线程:判死 + 聚合 + 刷缓存”registryMonitorThread 每 30 秒做四件事(JobRegistryHelper.java:72-141):
① findByAddressType(0) 只处理「自动注册」的组 :76② findDead(90s) → removeDead(ids) 物理删除超时行 :80-83 SQL: WHERE update_time < DATE_ADD(#{nowTime}, INTERVAL -#{timeout} SECOND)③ findAll(90s) → HashMap<appname, List<address>> :86-103 → Collections.sort(registryList) → join(",") → group.setAddressList() :110-118④ 全量重建 appname2GroupCache / id2GroupCache :123-138 仅当 GsonTool.toJson(新) != GsonTool.toJson(旧) 才替换引用 + 打 info 日志判死时间是 90 秒 = 心跳周期 × 3,标准的「三次心跳容错」。最坏情况下一个已经宕掉的执行器会在注册表里存活 90 秒,期间仍会被路由选中——所以路由层还需要 FAILOVER 这类探测式策略兜底。
第 ③ 步的 Collections.sort(registryList)(:111)容易被当成无意义代码,其实很重要:地址列表必须有确定顺序,否则 FIRST / LAST / 一致性哈希这些依赖下标或顺序的策略会在每轮刷新后跳变。
第 ④ 步的「JSON 比对后才替换」(:133-137)是一个廉价的变更检测:避免每 30 秒都打一条 info 日志、也避免无谓地丢弃已有的 Map 引用。
4. 两种地址来源
Section titled “4. 两种地址来源”address_type |
来源 | 由谁维护 |
|---|---|---|
0 自动注册 |
xxl_job_registry 聚合而来 |
registryMonitorThread 每 30s 覆写 |
1 手动录入 |
控制台里人工填的固定地址 | 完全不受注册线程影响(findByAddressType(0) 过滤掉了) |
最终 XxlJobGroup#getRegistryList() 只是把 address_list 按逗号切开(XxlJobGroup.java:27-33),两种来源在路由层完全等价。
单次触发还能临时覆盖地址——JobTrigger#trigger 里如果传了 addressList 参数,会就地把 group 改成手动模式(JobTrigger.java:83-86):
if (StringTool.isNotBlank(addressList)) { group.setAddressType(1); group.setAddressList(addressList.trim());}这是控制台「指定地址执行」和 OpenAPI triggerJob(addressList=...) 的实现,灰度验证时很有用。注意它改的是内存里的临时对象,不落库。
5. 优雅下线
Section titled “5. 优雅下线”ExecutorRegistryThreadHelper#stop 先停心跳线程,再主动发一次 registryRemove(:76-89),admin 侧执行 registryDelete 物理删行(JobRegistryHelper.java:212)。正常停机时地址会立刻消失,不用等 90 秒——前提是走了 DisposableBean#destroy。kill -9 就只能靠超时判死。
九种路由策略
Section titled “九种路由策略”ExecutorRouteStrategyEnum 里注册了 10 个枚举值,其中 9 个有 ExecutorRouter 实现,SHARDING_BROADCAST 的 router 是 null(ExecutorRouteStrategyEnum.java:20)——它在 JobTrigger 里被特判走另一条路,见日志回捞与分片广播。
┌─ 无状态 ─────────────────────────────────────┐ │ FIRST get(0) │ 集群一致 ✅ │ LAST get(size-1) │ 集群一致 ✅ │ RANDOM get(random) │ 集群一致 ✅(本就随机) ├─ 有状态(admin 进程内存)───────────────────┤ │ ROUND jobId → AtomicInteger 计数 │ 集群下退化 ⚠️ │ LRU jobId → LinkedHashMap(accessOrder)│ 集群下退化 ⚠️ │ LFU jobId → HashMap<addr, count> │ 集群下退化 ⚠️ ├─ 确定性映射 ────────────────────────────────┤ │ CONSISTENT_HASH md5(jobId) → 哈希环 │ 集群一致 ✅ ├─ 探测式 ────────────────────────────────────┤ │ FAILOVER 逐个 beat(),首个成功者 │ 集群一致 ✅ │ BUSYOVER 逐个 idleBeat(),首个空闲者 │ 集群一致 ✅ └──────────────────────────────────────────────┘无状态三兄弟
Section titled “无状态三兄弟”// ExecutorRouteFirst.java:15return Response.ofSuccess(jobGroup.getRegistryList().get(0));// ExecutorRouteLast.java:15return Response.ofSuccess(list.get(list.size() - 1));// ExecutorRouteRandom.java:21String address = addressList.get(localRandom.nextInt(addressList.size()));FIRST / LAST 之所以能稳定命中同一台,正是因为上文那句 Collections.sort()。
有状态三兄弟:集群下的陷阱
Section titled “有状态三兄弟:集群下的陷阱”三者的状态都存在 admin 进程的静态 Map 里:
| 策略 | 状态结构 | 位置 |
|---|---|---|
ROUND |
ConcurrentMap<Integer, AtomicInteger> jobId → 计数 |
ExecutorRouteRound.java:19 |
LRU |
ConcurrentMap<Integer, LinkedHashMap<String,String>> |
ExecutorRouteLRU.java:28 |
LFU |
ConcurrentMap<Integer, HashMap<String,Integer>> jobId → (地址 → 次数) |
ExecutorRouteLFU.java:26 |
三者共用同一套「防膨胀」套路:
// 24 小时全量清空一次if (System.currentTimeMillis() > CACHE_VALID_TIME) { jobLRUMap.clear(); CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24; // ExecutorRouteLRU.java:34-37}// 计数溢出保护 + 首次随机化if (count == null || count.get() > 1000000) { count = new AtomicInteger(new Random().nextInt(100)); // ExecutorRouteRound.java:30-32}new Random().nextInt(...) 的初始化不是随手写的,源码注释写明了目的:「初始化时主动 Random 一次,缓解首次压力」(ExecutorRouteRound.java:31、ExecutorRouteLFU.java:47)。否则所有任务的轮询都从 0 开始,第一次触发会全部打到 addressList.get(0)。
LRU 的实现是 LinkedHashMap 的教科书用法(ExecutorRouteLRU.java:47):
lruItem = new LinkedHashMap<>(16, 0.75f, true); // accessOrder = true...String eldestKey = lruItem.entrySet().iterator().next().getKey(); // 迭代器第一个 = 最久未访问return lruItem.get(eldestKey); // :71-72 ← get() 又把它移到末尾accessOrder=true 让 get() 自动把元素移到链表尾部,于是「迭代器的第一个」永远是最久未被选中的地址。取出即刷新,一行 get() 同时完成了「选择」和「更新 LRU 顺序」两件事。源码注释还顺带提了 removeEldestEntry 的用法,是很好的知识点锚。
LFU 则是全量排序取最小(ExecutorRouteLFU.java:64-68):
List<Map.Entry<String,Integer>> lfuItemList = new ArrayList<>(lfuItemMap.entrySet());lfuItemList.sort(Map.Entry.comparingByValue()); // 升序,取最小Map.Entry<String,Integer> addressItem = lfuItemList.get(0);addressItem.setValue(addressItem.getValue() + 1); // 选中即加一地址数通常是个位数,O(n log n) 完全可接受——但如果一个执行器组有几百个节点,这里每次触发都要排一次序。
private static final int VIRTUAL_NODE_NUM = 100; // :24TreeMap<Long, String> addressRing = new TreeMap<>();for (String address : addressList) for (int i = 0; i < VIRTUAL_NODE_NUM; i++) addressRing.put(hash("SHARD-" + address + "-NODE-" + i), address); // :69-75long jobHash = hash(String.valueOf(jobId)); // :78Map.Entry<Long,String> ceilingEntry = addressRing.ceilingEntry(jobHash); // :81return ceilingEntry != null ? ceilingEntry.getValue() : addressRing.firstEntry().getValue(); // :91hash() 用 MD5 摘要的前 4 字节拼成 32 位无符号数(:49-54),注释解释了为什么不用 String#hashCode:「String 的 hashCode 可能重复,需要进一步扩大 hashCode 的取值范围」(:19)。虚拟节点 100 个则是为了「解决不均衡问题」(:18)。
ceilingEntry(jobHash) 取环上顺时针第一个不小于 jobHash 的节点;取不到说明 jobHash 超过了环上最大值,回绕到 firstEntry()——这就是「环」的闭合处。
v3.4.0 Release Notes 第 4 条:「一致性哈希路由算法优化,重构哈希环逻辑提升代码简洁性」。旧版用
tailMap(jobHash)再取firstKey(),代码还留在注释里(:85-88),新版换成一句ceilingEntry语义更直白。
注意:哈希环是每次 route 都重建的(:69-75)——addressList 只有几个元素,建 100 × N 个 TreeMap 条目的成本可以接受,但换来的好处是完全无状态,天然对集群友好。这是拿 CPU 换正确性的典型交易。
探测式两兄弟
Section titled “探测式两兄弟”// FAILOVER:找第一个心跳通的 ExecutorRouteFailover.java:24-47for (String address : addressList) { beatResult = getExecutorBiz(address, jobGroup).beat(); if (beatResult.isSuccess()) return beatResult.setData(address);}// BUSYOVER:找第一个不忙的 ExecutorRouteBusyover.java:24-46for (String address : addressList) { idleBeatResult = getExecutorBiz(address, jobGroup).idleBeat(new IdleBeatRequest(jobId)); if (idleBeatResult.isSuccess()) return idleBeatResult.setData(address);}执行器侧的两个探针语义完全不同:
| 探针 | 实现 | 判定 |
|---|---|---|
beat() |
直接 return Response.ofSuccess()(ExecutorBizImpl.java:28-30) |
只证明进程活着且端口通,不关心负载 |
idleBeat(jobId) |
查 jobThreadRepository.get(jobId),isRunningOrHasQueue() 为真就返回失败(ExecutorBizImpl.java:33-46) |
证明这个 jobId 在这台机器上既没在跑、队列也是空的 |
idleBeat 判定的是单个 jobId 的忙闲,不是机器整体负载——因为 JobThread 是按 jobId 划分的(见阻塞策略与失败重试)。所以 BUSYOVER 的准确名字应该是「避开正在跑这个任务的节点」。
两者的代价是路由阶段引入了 N 次同步 HTTP 往返:最坏情况下 N 台机器全不可达,要串行等 N × 3 秒(xxl.job.timeout=3),而这一整段耗时都算在触发线程里——正是 JobTriggerPoolHelper 那个 500ms 慢池阈值要对付的场景。
九种策略都通过 XxlJobAdminBootstrap.getExecutorBiz(address, group) 拿客户端(:145-175),内部有一层缓存:
String cacheKey = address + "|" + accessToken + "|" + appname; // :155ExecutorBiz executorBiz = executorBizRepository.get(cacheKey);if (executorBiz != null) return executorBiz;executorBiz = HttpTool.createClient().url(address).timeout(...).header(...).proxy(ExecutorBiz.class);executorBizRepository.put(cacheKey, executorBiz);if (executorBizRepository.size() > EXECUTOR_BIZ_CACHE_MAX_SIZE) { // 1000 executorBizRepository.clear(); // :171-173 ← 全清,不是 LRU}超过 1000 条整表清空而不是淘汰最久未用的。粗暴但足够:地址数远小于 1000 时永不触发;真触发了也只是下次调用重建代理,无正确性影响。
为什么注册表放 MySQL,而不是内存 + 集群同步
Section titled “为什么注册表放 MySQL,而不是内存 + 集群同步”替代方案:调度中心各节点在内存里维护注册表,节点之间用 gossip / Distro 协议同步(Nacos 的做法)。
为什么不行:调度中心本来就是一个「必须有 DB」的应用——任务定义、日志、报表全在 MySQL。为注册表单独引入一套集群同步协议,等于给一个已有共享存储的系统再造一套弱一致存储,收益(少一次 DB 写)远小于成本(一致性 bug、脑裂处理、运维复杂度)。
证据:整个注册链路的代码量说明问题——执行器侧 109 行、admin 侧 244 行,没有任何一行处理节点间同步。心跳「任一 admin 成功即 break」(ExecutorRegistryThreadHelper.java:58)之所以成立,正是因为写进了共享 DB。
为什么 ROUND / LRU / LFU 明知集群下会失效还保留
Section titled “为什么 ROUND / LRU / LFU 明知集群下会失效还保留”问题:三者的状态在 admin 进程内存里。调度中心部署 3 个节点时,某个 jobId 的轮询计数在三个节点上各算各的——实际效果退化成「三条独立的轮询序列叠加」,负载不再均匀,LRU/LFU 的「最久未用 / 最少使用」判断也只反映本节点视角。
为什么不修:修的代价是把计数状态外置(Redis 或 DB),那就等于给每次路由加一次网络往返,而路由本身要求极低延迟。
替代方案:需要集群下确定性时用 CONSISTENT_HASH(无状态、纯函数);需要真实负载感知时用 BUSYOVER(探测真实状态)。这两个策略的存在,本身就是对前三个策略局限性的补偿。
结论:这不是 bug,是「本地近似 vs 分布式精确」的显式取舍——但文档里没写,只能从代码里读出来。
为什么 freshGroupRegistryInfo 被留成空方法
Section titled “为什么 freshGroupRegistryInfo 被留成空方法”方法体里只有一行注释:// Under consideration, prevent affecting core tables(JobRegistryHelper.java:224)。
它原本要做的:执行器首次注册成功(ret == 1)时,立刻把新地址刷进 xxl_job_group.address_list,让扩容的节点马上能接到任务。
为什么放弃:xxl_job_group 是核心表,被 JobTrigger 每次触发都要 load 一次(JobTrigger.java:80)。注册是高频操作(N 个执行器 × 每 30 秒),如果每次首注册都去 UPDATE 这张热表,容易和触发路径的读产生锁竞争。
代价:扩容后最长 30 秒才有流量。作者选择了牺牲扩容实时性,换取核心表的写入平稳——对一个「分钟级调度」的系统来说,这个交易是划算的。
| 参数 | 默认值 | 含义 | 什么时候要改 |
|---|---|---|---|
Const.BEAT_TIMEOUT |
30 秒(硬编码) | 心跳周期 = 注册监控周期 | 不可配。改要同时改两侧 jar |
Const.DEAD_TIMEOUT |
90 秒(= 30 × 3) | 判死阈值 | 同上 |
xxl.job.executor.ip |
空(自动探测) | 上报的 IP | 容器/多网卡环境必须显式配,否则上报内网 IP |
xxl.job.executor.port |
样例配 9999;留空则从 9999 起找空闲 | 内嵌 Netty 端口 | 需要固定端口做安全组放行时配 |
xxl.job.executor.address |
空 | 直接指定完整注册地址 | 有反向代理/NodePort 时用,优先级最高 |
xxl.job.executor.accessToken |
default_token |
双向鉴权令牌 | 3.x 起为空会直接启动失败(XxlJobExecutor.java:143-145),生产必须改掉默认值 |
xxl.job.executor.excludedpackage |
空 | 追加 @XxlJob 扫描排除包 |
大型应用里跳过明显无任务的包,减少扫描开销 |
EXECUTOR_BIZ_CACHE_MAX_SIZE |
1000(硬编码) | 客户端代理缓存上限 | 不可配 |
VIRTUAL_NODE_NUM |
100(硬编码) | 一致性哈希虚拟节点数 | 不可配 |
| 场景 | 现象 | 原因 | 规避 |
|---|---|---|---|
容器化部署未配 address |
注册成功,但触发全部超时 | 自动探测到容器内网 IP,调度中心路由不到 | 显式配 xxl.job.executor.address 为 Service/NodePort 可达地址 |
| 扩容新执行器 | 30 秒内不接任何任务 | freshGroupRegistryInfo 是空方法,要等监控线程刷 address_list |
接受它;或缩短观察期时用「指定地址」手动触发验证 |
kill -9 停执行器 |
90 秒内仍被路由选中,触发全部失败 | 没走 registryRemove,只能超时判死 |
用 SIGTERM;路由策略配 FAILOVER 兜底 |
调度中心集群 + ROUND 策略 |
负载不均,某台执行器明显偏多 | 轮询计数是各 admin 节点的进程内状态 | 换 CONSISTENT_HASH 或 BUSYOVER |
用 LFU 且执行器数很多 |
触发耗时上升,慢池被打满 | 每次路由都要对全量地址排序 | 地址数 > 数十时改用 CONSISTENT_HASH |
FAILOVER + 大批执行器全挂 |
单次触发耗时 N × 3 秒,调度整体变慢 |
串行 beat() 探测,每个都要等超时 |
保证执行器组规模可控;配合慢池分流已能兜住 |
同一 appname 配了不同 accessToken |
部分节点注册被拒 | admin 侧按 appname 查 group 再比对 token(OpenApiController.java:62-65) |
同组统一配置 |
| 一致性哈希下缩容一台 | 只有该节点承担的任务被重新分配 | 这正是一致性哈希的目的 | 无需处理,但注意任务与节点的绑定关系会变,本地缓存类任务要考虑冷启动 |
| 手法 | 在这里的样子 | 别处怎么用 |
|---|---|---|
唯一索引 + ON DUPLICATE KEY UPDATE 做 upsert |
一条 SQL 表达注册与续约 | 任何「首次插入 / 后续更新」的幂等写。比 SELECT 后判断再写少一次往返,且无并发竞争窗口 |
| 心跳周期 × 3 = 判死阈值 | 30s / 90s | 通用的心跳容错比例。允许连丢两次,第三次才判死,在网络抖动与故障发现速度之间取平衡 |
| 地址列表必须排序 | Collections.sort(registryList) |
任何依赖下标或顺序的负载均衡(轮询、哈希环)都必须先固定顺序,否则每次刷新都会重排流量 |
| 无状态路由 > 有状态路由 | 一致性哈希每次重建环,换来集群一致 | 负载均衡器多副本部署时,本地计数状态必然发散。要么接受近似,要么用纯函数映射 |
| 两级探针:存活 vs 空闲 | beat() 只测端口,idleBeat() 测具体任务的忙闲 |
健康检查要分层:liveness(进程活着)和 readiness(能接活)语义不同,混用会导致「活着但接不动」的节点持续被打流量 |
| 有意保留的空方法 + 原因注释 | freshGroupRegistryInfo |
比删掉更好——它记录了「这里曾经想做什么、为什么没做」。这类注释是代码考古的路标 |
面试锚点
- 执行器心跳 30 秒、判死 90 秒,这个 3 倍关系是怎么来的?改成 1 倍会怎样?
- 调度中心部署 3 个节点时,哪些路由策略会退化?为什么一致性哈希不会?
beat()和idleBeat()分别在探测什么?BUSYOVER判断的是机器负载还是别的?- 执行器注册用了什么 SQL 做幂等?为什么不用「先查再写」?
- 新扩容的执行器为什么要等 30 秒才有流量?源码里哪一处决定了这一点?
Related
Section titled “Related”- 整体架构:注册与路由在全局时序里的位置(步骤 ⑦~⑨)
- DB 锁与秒级时间轮:谁在调用路由,以及慢池阈值为什么是 500ms
- 阻塞策略与失败重试:
idleBeat依赖的JobThread模型 - 日志回捞与分片广播:
SHARDING_BROADCAST为什么不走ExecutorRouter - 面试专题