Skip to main content

BLOGE 0.9.8-RC1 在线版 · 事实校验 2026-09-15 · English

第 21 章 —— 单 JVM 里的调度与复杂度

本章承诺: 你会用一笔订单的四节点 graph 看清 BLOGE 如何找到就绪节点、并发执行、等待汇合,并能解释复杂度护栏保护的究竟是什么。

学习目标

学完本章,你能够:

  1. 画出一轮完成驱动调度中 ready set 的变化。
  2. 区分 graph 间并发、graph 内并发和共享资源容量。
  3. 解释 DependencyScheduler 为什么按出边更新下游,而不是反复扫描整个 graph。
  4. 用节点数、深度、扇出和分支嵌套判断复杂度风险。
  5. 写出一次能复核的单机测量说明,而不是只给一个 QPS 数字。

这一章只增加一个变量

前面已经知道订单 graph 会按依赖关系执行。本章把问题缩小到一台机器、一个 JVM、一个 GraphEngine当多个节点同时具备运行条件时,引擎如何推进?

租约、分片、远端 worker 和数据库容量全部留到下一章。先把本地机制看清,否则分布式问题会把调度问题淹没。

开场:四个订单步骤,谁能先跑

订单处理有四个节点:

loadOrder ─┬─→ checkInventory ─┐
└─→ checkCredit ─┴─→ reserveOrder

loadOrder 完成后,checkInventorycheckCredit 谁先结束并不确定;但 reserveOrder 必须等两者都结束。先写下你的预测:每一个时刻,ready set 中有哪些节点?

图 21-1:节点完成驱动 ready set 向前推进

用 BLOGE 自己的测试观察调度合同

运行 RC1 的调度与复杂度聚焦测试:

cd submodule/bloge
mvn -pl bloge-core \
-Dtest=SchedulerSequenceTest,GraphComplexityValidatorTest test

2026-09-14 在 commit cc38fbe5 上观察到:

GraphComplexityValidatorTest Tests run: 18, Failures: 0
SchedulerSequenceTest Tests run: 12, Failures: 0
Tests run: 30, Failures: 0, Errors: 0, Skipped: 0

其中 fanInFromMany_waitsForAll 让四个前驱通过 latch 同时到达,再断言 sink 才能运行;multiDiamond_orderingConstraints 则只断言必须成立的先后关系,不要求并发节点有固定顺序。

这正是调度合同:依赖顺序确定,同层完成顺序可以变化。

机制:不是定时扫描,而是完成事件推进

每次执行拥有自己的 DependencyScheduler。初始化时,它为每个节点记录未完成依赖数,并建立 outgoingEdges 索引:

pendingDeps(loadOrder) = 0
pendingDeps(checkInventory) = 1
pendingDeps(checkCredit) = 1
pendingDeps(reserveOrder) = 2

第一轮只有 loadOrder 进入 ready queue。它完成后,调度器沿两条出边把两个计数减到零,于是两个检查节点进入 ready queue。任一检查完成只会把 reserveOrder 从 2 减到 1;第二个完成才把它减到零。

图 21-2:每次执行拥有独立调度状态,完成事件只触达直接下游

这种设计的关键不是“使用了虚拟线程”,而是推进成本与刚完成节点的出度相关。若每次完成都扫描全部边,大 graph 会反复支付无关检查成本。

三种并发不要混在一起

问题所有者本章能证明什么
多笔订单同时执行宿主调用方 + engine每次执行状态彼此隔离
一笔订单内并行检查dependency scheduler无依赖或依赖已满足的节点可并行
数据库/API 能否承受外部资源与容量配置本章不能证明

GraphEngine 可以被多个调用共享,不等于下游连接池无限。1000 个虚拟线程等待一个 20 连接的数据库,瓶颈仍是 20 个连接。

复杂度护栏:保护反馈速度,而不是限制想象力

RC1 在 graph 构建时检查四个维度。默认推荐/硬上限由 ComplexityLimits.DEFAULT 给出:

维度推荐上限硬上限风险直觉
operator 节点数1530状态和失败面扩大
DAG 深度610关键路径变长
单节点扇出58瞬时并发放大
分支嵌套23路径组合难以理解

超过推荐值产生 warning;超过硬上限拒绝 graph。聚焦测试实际覆盖了 31 节点、11 层深度和 9 路扇出的拒绝路径。

单因素破坏:把扇出从 8 改成 9

保持 operator、输入和其他边不变,只给 root 增加第九个 child。GraphComplexityValidatorTest 断言构建失败,消息包含 graph、root、hard limit 8 和拆分建议。

恢复方式有三种,按优先级判断:

  1. 检查九个任务是否真属于一个 graph,能否抽成子图或批处理。
  2. 检查是否把纯数据投影错误建成 operator 节点。
  3. 业务确实需要时,显式调整 limits,并为该拓扑建立测量基线。

直接提高上限只能让 graph 合法,不能让它更快、更容易调试。

如何写一份可信的单机测量记录

一个 QPS 数字至少要带上:commit、JDK、CPU 配额、heap、graph 拓扑、operator 行为、并发度、warmup、measurement、fork 数和原始结果文件。缺少这些字段,数字只能是当时电脑上的传闻。

测量时逐层增加变量:

空 operator 调度成本
→ 固定拓扑并发
→ 真实 operator(仍用受控依赖)
→ metrics / tracing 开销

先找到哪一层改变了结果,再谈优化。不要把一次功能测试的 elapsed time 当 benchmark,也不要把 JMH 的平均值当生产容量承诺。

现实映射:餐厅出餐板

后厨不会每秒扫描所有订单,问每道菜能否开始。某道前置菜完成时,它只通知直接依赖它的工位。冷菜与热菜可能并行,摆盘必须等两边到齐。

  • ready queue 是“现在可以做”的工位卡。
  • pendingDeps 是还缺几道前置工序。
  • outgoingEdges 是完成后要通知谁。
  • 复杂度护栏是在一张单据过大时要求拆桌或拆批。

餐厅有很多厨师不代表烤箱容量无限,对应到系统里就是线程并发不等于外部资源容量。

轮到你:画出并测量自己的 ready set

选择一条 4—8 节点 graph:

  1. 只画依赖边,标出初始 ready set。
  2. 任选一种合法完成顺序,逐步更新 pendingDeps
  3. 只增加一条边或一个 child,预测并运行复杂度检查。
  4. 建立最小 JMH 或重复测量记录,写全环境与拓扑。

产物是一张四帧 ready-set 图和一份测量卡。停止条件是别人能从卡片复现实验,并明确这个结果只代表固定单机环境。

本章证明了什么,没有证明什么

本章的 RC1 测试证明:被覆盖拓扑满足依赖顺序,fan-in 等待全部前驱,默认复杂度边界按预期拒绝过大 graph。它没有证明你的 operator 线程安全、外部 API 有容量、数据库不会成为瓶颈,也没有给出可复用的生产 QPS。

下一章只增加一个变量:第 22 章——分布式运行与容量验证会检查多 JVM 共享 durable 状态后的所有权、路由和容量瓶颈。

实验验收卡

  • 预期与观察: ready set 与完成事件驱动单 JVM 调度,fan-out 护栏只提示反馈成本。
  • 失败与恢复: 把 fan-out 从 8 改成 9;恢复结构或记录接受理由。
  • 证明边界: 证明调度合同和复杂度信号,不证明通用 QPS。
  • 练习合同: 四节点图;只加一条 fan-out 边;交付时间线和测量卡;结论仅覆盖本机单轴实验即停止。

精确事实入口

Coding Agent: Open the versioned task guide.