🪁内存与性能
版本支持:v2.16.1+
# 内存模型
| 数据 | 位置 | 何时驻留 |
|---|---|---|
规则清单、版本戳和状态索引(chainId → state、nodeId → state + 元数据) | JVM常驻 | 整个生命周期都在,条目数随规则总量线性增长 |
影子Chain和Node对象 | JVM常驻 | 每条启用的清单记录对应一个轻量对象,内容还没加载时也存在 |
| EL文本和编译后的条件树 | JVM有界缓存 | 命中时驻留,Caffeine按访问热度淘汰之后退回影子状态 |
| 脚本源码和编译产物 | JVM有界缓存 | 同上,通过chain的引用计数联动淘汰 |
这里有个关键性质要理解:正文和编译产物的常驻规模由缓存容量限制,但JVM的总内存仍然和规则总量有关。
举个例子,库里有10万条规则,热点只有200条,那么JVM不会常驻10万条EL和脚本正文以及它们的编译产物,但仍然会常驻10万个影子对象及其状态索引。
注意
清单很大的话,上线前请用真实的规则规模做一下堆内存和启动Manifest的基准测试,不要只按cache.capacity来估容量。
# 执行热路径
execute2Resp(chainId)
→ FlowBus.getChain(chainId) // 本地 map 查找
→ 已编译 → 直接执行 // 零远程调用
→ 否则(影子 / 收到变更后被失效)→ Chain 上 double-checked locking:
repository.fetchChain(chainId) // 一次远程读,带 fetch-retry-times 重试
→ LiteFlowChainELBuilder 构建条件树
→ 写入缓存,登记引用的脚本节点(引用计数 +1)
→ 子链引用(chain 调 chain)递归同一懒加载路径
→ 执行到脚本节点且执行器无产物:
per-node double-check → fetchScript → loadScript → 缓存产物
一致性是靠失效驱动的,不是读的时候去校验。热路径上不会逐次比对版本,而是等变更同步(watch、轮询或对账)到达时,把对应的缓存态置为失效,下次执行就走懒加载分支。所以缓存命中的执行路径,性能和原有模式基本没差别。
# 调优建议
cache.capacity:按你的热点chain条数来估,默认500对大多数应用够用了。设小了会频繁淘汰、频繁回源,设大了多吃堆内存。脚本没有独立的容量参数,它跟着chain联动淘汰,chain被淘汰时它引用的脚本引用计数减一,归零了就一起清掉。cache.preload-chain-ids:把首屏或者高QPS的关键chain列在这里,启动时就立即拉取并编译,可以抹平冷启动的尖刺。非关键链路不用预热,懒加载就够了。- 路由chain:
executeRouteChain为了拿到route元数据,会在路由执行前把所有还没就绪的Rule-DB chain逐个回源并编译,而不是只加载最终匹配的那一条。所以清单大又用路由模式的话,请把第一次路由请求当成一次批量冷加载,压测一下它的延迟,并考虑启动预热或者拆分application-name。 sync.poll-seconds:这项对SQL、PostgreSQL、MongoDB、Redis生效,觉得3秒不够及时可以调小,代价是序号查询更频繁。zk和etcd用watch,Nacos用Listener,这项对它们无效。sync.reconcile-seconds:60秒是个经验值,它本来就是极端情况下的兜底周期,调小意义不大,反而增加全量diff的开销。- Nacos的Catalog体积:每次冷读取和对账都要处理整份Catalog。建议通过拆分
application-name来控制单应用的规则量,同时监控配置体积、冷加载耗时和发布冲突率。Catalog大了不该只靠调高服务端上限硬撑。
# v1的实现注记:惰性刷新与last-good
已驻留的条目收到变更通知后会标记为待刷新,但已经成功激活的条件树和脚本产物不会立即销毁。
下次执行时,会在总线外回源并编译候选版本:
- 编译成功就原子替换。
- 失败则状态记为
FAILED,desiredVersion保持新版而activeVersion保持旧版,旧的last-good generation继续执行。 - 只有那种一个已激活版本都没有的冷规则加载失败了,执行才会抛出加载异常。
不管哪种情况,正在执行中的请求始终持有原引用跑完。
所以收到变更之后的第一次执行,仍然可能承担回源和编译的延迟;而候选版本损坏时,后续执行会继续尝试加载新版。至于后台预编译、消除首个请求延迟这种能力,当前版本还没有。
帮助我们改善此文档 (opens new window)



