Lazy loaded image
开源贡献
Dubbo Admin流量规则版本控制功能测试报告
字数 4668阅读时长 12 分钟
2026-5-22
type
Post
status
Published
date
May 22, 2026
slug
dubbo-admin-reviewer-report
summary
tags
推荐
category
开源贡献
icon
password
 
日期: 2026-05-22

版本信息

组件
版本
dubbo-admin (branch)
feat/Support-version-history-and-rollback-for-traffic-rules @ ae5ed03
Go
1.21
Vue3 / ui-vue3
与 develop 同步
MySQL
8.0 (Docker issue1473-mysql, 127.0.0.1:3306)
ZooKeeper
3.9 (Docker issue1473-zk, 127.0.0.1:2181)
浏览器
Chrome 124
测试规则
demo.condition-router / demo.tag-router / demo.configurator (mesh = zk-smoke)

功能概述与价值

本功能围绕 Apache Dubbo Admin 的流量规则(Condition Route / Tag Route / Dynamic Config)落地不可变发布账本与一键回滚。核心思路是:在 Admin 写入链路上先创建一条 rule version intent,再执行原有规则写入,最后把 intent 提交为不可变版本行;注册中心直推则由同步 RuleVersionSubscriber 捕获并以 UPSTREAM 入账。原有 CRUD 主链路(handler → governor → ZK)保持兼容,失败时整条版本能力可一键关停。注: intent 设计类似于数据库里的 pending transaction,用于解决“规则写入”和“历史记账”之间没有天然事务的问题。
对运维 / SRE 侧,价值是误改可恢复、上游变更有审计、并发编辑会被提示而不是静默覆盖;对开发侧,价值是版本能力通过 intent 与 subscriber 接入现有写入链路,不改变原有规则 API 的基本行为,可通过单一开关 versioning.enabled 即时下线整个能力。

1. 架构概述

  • Admin UI: 在每条规则详情页提供 History 抽屉 + Monaco diff + Rollback modal;并发异常时提供 sticky 通知,对 pending intent 提供 Repair / Abandon 操作。
  • Admin Backend: 新增 pkg/core/versioning 包;每种规则一组 4 个版本端点(versions / versions/:id / versions/:id/diff / versions/:id/rollback),三种规则共 12 个规则版本路由;另新增 2 个 intent 恢复路由(/rule-version-intents/:intentId/repair / /rule-version-intents/:intentId/abandon)。
  • ZooKeeper: 注册中心,所有规则写入实际落到 ZK 节点,Admin 通过订阅事件回声入册。
  • MySQL: 承载 rule_version(不可变发布行)、rule_version_meta(当前版本指针与版本号计数器)与 rule_version_intent(写入中间态)三张表。
本次范围内支持三类规则:Condition RouteTag RouteDynamic Config (Configurator)。AffinityRoute 因不在 governor.RuleResourceKinds 中,本次不在范围。

2. 测试结果总览

#
测试项
状态
说明
1
Bootstrap 基线入册
PASS
启动后存量规则各产生一条 BOOTSTRAP 行,author=system:bootstrap
2
Admin 编辑入册
PASS
保存后 ledger 新增 ADMIN 行,isCurrent 切换
3
上游 ZK 推送入册
PASS
注册中心直推 → UPSTREAM 行,author=system:zookeeper
4
Diff 当前
PASS
/diff?against=current 返回 left/right 两侧 JSON
5
Rollback 生成 ROLLBACK 行
PASS
旧 snapshot 重发,新行 source=ROLLBACKrolled_back_from_id 指源
6
Rollback reason 必填
PASS
空 reason 返回 HTTP 400 InvalidArgument,不入册
7
乐观锁 409
PASS
expectedVersionId 不匹配返回 HTTP 409 VERSION_CONFLICT + currentVersionId;存在未闭合 intent 时返回 VERSION_LEDGER_PENDING + intentId
8
409 sticky 通知
PASS
前端 notification.warning({duration:0}) 不自动消失,冲突带 Reload,pending intent 带 Repair / Abandon
9
Retention trim
PASS
maxVersionsPerRule=3 时只保留最新 3 行,version_no 不重用
10
FEATURE_DISABLED 端点
PASS
versioning.enabled=false 时所有版本端点返回 HTTP 503
11
跨规则类型回滚
PASS
tag-rule、configurator 与 condition-rule 行为一致
12
单测套件
PASS
核心版本包、console handler/service、config 相关测试通过
13
go vet / npm run build
PASS
后端 vet 无新增告警;前端构建通过
14
端到端 drill 测试
PASS
TestE2ERollbackDrill 串联 bootstrap → admin intent → upstream → rollback intent → trim
15
Owner code review
PASS
多轮 owner review 后,旧 hint API 已删除,并补齐 intent 修复、pending 状态、reason 长度、同步 subscriber 等关键边界

3. 版本账本核心功能验证

Bootstrap(作者) 初始化默认基线:
Bootstrap指
Admin 编辑+Diff当前:
Rollback + 空 reason 拒绝:
并发 409 sticky 通知:
ZK 直推 → UPSTREAM 入账
默认5个版本测试:

3.1 Bootstrap 基线入册

启动后端,pkg/core/versioning/component.go::Start 会先尝试修复未闭合的 open intent,再扫描所有现存规则。RecordBootstraprule_version_meta 中尚无记录的规则插入一条 source=BOOTSTRAP 基线(幂等;若 meta 已存在则跳过)。
调用:
返回(01-bootstrap-versions.json,关键字段):
total=1,首次启动后恰好一条 baseline。

3.2 Admin 编辑入册

UI 上修改 priority 后保存。后端流程:handler → service.prepareRuleMutation → service.BeginMutationIntent(SourceAdmin) → ResourceManager.Update → governor → ZK 写入 → MarkMutationIntentApplied → CommitMutationIntent → InsertVersion(source=ADMIN)。如果注册中心事件先回声到 subscriber,subscriber 也会按 content hash 匹配 open intent 并提交为同一条 ADMIN 版本,避免重复入账。
响应:
随后查询 ledger,取自 02-versions-after-update-retry.json。该快照取在一次回滚验证之后,因此 ledger 中已含一条 ROLLBACK 历史行,这恰好印证 ledger 的 append-only 性质:
证据:02-versions-after-update-retry.json(total=3)。

3.3 上游 ZK 推送入册

通过 ZK 客户端直接修改注册中心节点(基础路径 /dubbo/config,节点名 = 规则名):
ZKConfigEventSubscriber 接到节点变更后写入本地 store,并发出带 Context()["source-registry"]="zookeeper" 的事件。RuleVersionSubscriber 接到事件后,会先尝试按 content hash 匹配 open intent;若没有匹配到 admin / rollback intent,则按 source=UPSTREAM 落 ledger,author 从事件 context 推导为 system:zookeeper
ledger 顶部多出一行:
证据:07-upstream-versions.json

3.4 Diff 当前

返回:
前端 RuleDiffEditor.vue 用 Monaco diff 渲染为左右两栏。证据:03-diff-retry.json + UI 截图。

3.5 Rollback 生成 ROLLBACK 行

以联调实测为例,ledger 当前 head 为 v3 ADMIN(底层有 v2 ADMIN 与 v1 BOOTSTRAP),回滚到 v2:
服务端流程:service.Rollback → withRuleLock → prepareRuleMutation → BeginMutationIntent(source=ROLLBACK, rolledBackFromID=2) → rm.Upsert(v2 snapshot) → MarkMutationIntentApplied → CommitMutationIntent → InsertVersion(source=ROLLBACK)。若存在未闭合 intent,会优先返回 VERSION_LEDGER_PENDING;若 expectedVersionId 已落后,返回 VERSION_CONFLICT
ledger 新增一行(取自 04-versions-after-rollback-retry.json):
v2 ADMIN 行未被改写;ledger 上 v1 / v2 / v3 / v4 四行并存。v2 的 spec 通过新的 v4 行重新发布,而不是覆盖原 v2。
证据:04-versions-after-rollback-retry.json(total=4)。

4. 并发与边界

4.1 乐观锁 409

Tab A 与 Tab B 同时加载规则,两者持有相同的 expectedVersionId=5。A 先保存(PUT 携带 expectedVersionId=5)成功,current_version 推进到 6。B 再保存(仍带 expectedVersionId=5),服务端 CheckExpectedVersion 失败:
证据:05-409.http
注意:expectedVersionId 仍不是数据库级强 CAS,但当前实现已经比早期版本更实。服务端会先检查同规则是否存在 open intent,再检查 rule_version_meta.current_version;因此同一规则窗口内的第二个写入会优先得到 VERSION_LEDGER_PENDINGVERSION_CONFLICT,不会只依赖异步 meta 更新。强 CAS / 多实例真实数据库并发压测仍可作为后续议题。
若存在未完成版本 intent,服务端返回:
前端会基于 intentId 给出 Repair / Abandon 操作。

4.2 Rollback reason 必填

修复前服务端正确抛了 bizerror.InvalidArgument,但 handler 默认分支将所有 bizerror 映射成 HTTP 200 + UnknownError,前端看到的是模糊的错误。修复方法:在 writeVersioningResp 增加 InvalidArgument → 400 + 原 code 映射,并在 handler 层补充 reason 最大长度校验。新增 TestWriteVersioningRespMapsInvalidArgumentToBadRequest 与 reason 长度相关单测。
证据:04-empty-reason-rejected-fixed.http

4.3 Retention trim(maxVersionsPerRule)

配置 versioning.maxVersionsPerRule=3 后,连续编辑多次,ledger 总计累积到 v11,最终 ledger:
version_no 单调,trim 后不重用:v1–v8 已永久丢弃,后续插入只会推进到 v12、v13…而不会回收已释放的号段。
证据:06-retention-versions.json(total=3,因 maxVersionsPerRule=3 只保留最新 3 行)。

4.4 FEATURE_DISABLED 端点

versioning.enabled=false 重启后:
CRUD 端点行为完全不受影响。证据:01-versions-disabled.http

5. 错误处理与可用性

5.1 sticky 409 通知

前端同时处理 VERSION_CONFLICTVERSION_LEDGER_PENDING。冲突场景的逻辑(ui-vue3/src/views/traffic/_shared/ruleVersion.ts::notifyVersionConflict):
duration: 0 让通知持续显示直到用户主动关闭;Reload 按钮直接触发表单重新加载当前版本。
对于 VERSION_LEDGER_PENDING,前端会读取响应里的 intentId,展示另一条 sticky 通知,并提供 Repair / Abandon 按钮。Repair 会调用:
Abandon 会调用:
这避免了“账本中间态卡住后只能刷新重试”的体验问题。

5.2 Rollback 空 reason 拒绝(UI 端)

UI 上点回滚,弹出 modal 后清空 reason 提交:
  • 前端会先用 message.warning('请填写回滚原因') 阻止空 reason 提交。
  • 后端仍保留兜底校验,空 reason 返回 HTTP 400 InvalidArgument
  • reason 超过 1024 字符时,handler 层返回 InvalidArgument: reason must be at most 1024 characters

6. 代码审查发现

PR 进入合并阶段前,我以项目 owner 视角对全分支做了四轮代码审查。早期 review 暴露的 hint / coalesce 方案已在后续提交中替换为 intent 驱动实现;round-3 / round-4 继续补齐 pending intent、同步 subscriber、reason 校验与前端异常处理等边界。

6.1 决策矩阵

类别
数量
处理
FIX
多项
已在本 PR 内修复,包括删除旧 hint API、补齐 intent 恢复、修复 bootstrap 并发 meta 插入、同步 subscriber 入账、reason 长度校验等
DOCUMENT
多项
在 PR 描述或代码注释中说明语义,包括 effective state change log、weak CAS、409/503 响应形态等
WON’T FIX / DEFER
多项
对不属于本 PR 的系统性议题显式延期,如大规模 bootstrap batch、非 ZK registry source context、多实例真实数据库压测等
详细列表(节选最具代表性的几条):
#
类别
finding
决策
1
死码
AdminHintRegistryPutAdminHintRecordMutation 等旧 hint API 已不在生产路径
FIX(删)
2
测试真实性
TestE2ERollbackDrill 早期依赖 dead API,未覆盖生产 intent 路径
FIX(改为 intent 驱动)
3
输入校验
Diff(against=...) 解析允许尾随脏字符
FIX(strconv.ParseInt)
4
并发
bootstrap 并发创建 meta 行可能竞争
FIX(GORM store 使用 conflict + re-select for update)
5
并发
admin/rollback 写入与未闭合 intent 竞争时,后续 mutation 可能继续推进
FIX(OpenIntent 前置检查 + VERSION_LEDGER_PENDING)
6
审计语义
上游事件与 admin intent 竞争时不能静默丢事件
FIX(提交 intent 失败则 fallback 为 UPSTREAM 入账)
7
错误处理
reason 长度 unchecked,可能落到 DB 错误
FIX(handler 层限制 1024)
8
前端
409 只覆盖 VERSION_CONFLICT,未覆盖 ledger pending
FIX(新增 Repair / Abandon 通知)
9
前端
Monaco diff setModel 未释放旧 model
FIX(dispose 旧 model)
10
前端
RuleDiffEditor 默认 editorId 全局 getElementById footgun
FIX(改 template ref)
11
文档
expectedVersionId 只写弱 CAS,低估 open-intent check 的保证
DOCUMENT(补充说明)
12
范围
Nacos / Apollo 未携带 SourceRegistryContextKey
DEFER
13
性能
bootstrap O(rules) 顺序事务
DEFER
14
范围
AffinityRoute 不在 governor.RuleResourceKinds
DEFER

6.2 关键修复(详)

6.2.1 Hint registry 替换为 mutation intent

早期实现里,Admin 写入通过 AdminHintRegistry 标记来源,subscriber 收到事件后再通过 hint 判定 ADMIN / ROLLBACK / UPSTREAM。review 后发现这套 API 已经被 intent 路径取代,生产路径并不需要保留 hint 注册表;继续保留只会让 PR 描述与真实实现不一致。
修复:删除 AdminHintRegistryPutAdminHintRecordMutation 等旧 API,统一改为 BeginMutationIntent → MarkMutationIntentApplied → CommitMutationIntent。subscriber 收到事件时先按 content hash 匹配 open intent,匹配成功则提交 intent;匹配失败则按上游事件正常入账。

6.2.2 Pending intent 恢复与 graceful shutdown

旧稿中提到的 coalesce pending 已不再存在,当前 RuleVersionSubscriber.AsyncEnabled() 返回 false,事件同步处理,FlushAll 不再承担批量刷盘职责。
真正需要处理的是 open intent:进程启动时,component.Start 会先调用 repairOpenIntents;mutation 前也会通过 repairPendingIntent 尝试把已经落到 ResourceManager 的 pending intent 修复成版本行。如果资源状态仍不匹配,接口返回 VERSION_LEDGER_PENDING,前端提供 Repair / Abandon。

6.2.3 Monaco diff 内存泄漏

RuleDiffEditor.vue::render() 在 props 变化时 monaco.editor.createModel(...) 两次后 diffEditor.setModel(...),旧 model 未 dispose。每次切换版本对比都泄漏两个 ITextModel
修复:setModel 前 dispose 旧 model 对;editorId 由 prop 改为 Vue template ref。

6.3 显式不修(WON’T FIX)

| # | finding | 理由 | | --- | --- | | — | GormStore 全局 sync.Mutex 串行化所有 ledger 写入 | admin 写入 QPS 在个位数,锁竞争不构成瓶颈;保留作为 SQLite/gorm 兼容兜底 | | — | Sanitize/Validate 部分判断重复 | 职责不同(改默认 vs 报错),loader 调用次序明确 | | — | 三层 XxxRule → WithOptions → Unsafe 命名”Unsafe”误导 | 分层有用(向后兼容 + 锁/无锁分离),仅命名差,延后重构 | | — | 预存在的 configurator_rule.go::PutConfiguratorWithRuleNamereturn | 不是本 PR 引入的回归 | | — | 409/503 响应体用扁平 gin.H 而不是统一 CommonResp | 前端拦截器依赖顶层 currentVersionId / intentId,当前形态是刻意保留 | | — | rollback / rule mutation 只有登录态校验,没有细粒度 RBAC | 与现有规则 CRUD 权限模型一致;RBAC 是项目级议题,不在本 PR 内解决 |

7. 复现步骤

7.1 环境准备

7.2 配置文件

后端配置 dubbo-admin.yaml 关键部分(与本次联调实际使用的 dubbo-admin-smoke.yaml 同构):

7.3 启动服务

7.4 验证服务就绪

7.5 验证 BOOTSTRAP 基线生成

7.6 验证回滚

8. 配置注意事项

配置项
推荐值
说明
versioning.enabled
true
默认 false;false 时所有版本端点返回 503,CRUD 不受影响
versioning.maxVersionsPerRule
5
trim-on-insert;<=0 会在 sanitize/display 场景回退默认,配置校验要求大于 0
store.type
mysqlpostgres
生产请用持久化数据库;未配置数据库连接时组件会退回 memory store,仅适合开发/测试
expectedVersionId 在所有 mutation 端点(PUT/POST/DELETE)与 /rollback 上都是可选:不传 → 行为完全不变;传 → 触发乐观版本检查。当前检查会先看 open intent,再看 rule_version_meta.current_version,因此可能返回 VERSION_LEDGER_PENDINGVERSION_CONFLICT

9. 结论

  1. 版本账本核心功能完整:Bootstrap、Admin、Upstream、Rollback 四类来源全部正确入册,version_no 单调且不重用。
  1. 并发与边界保护:乐观锁 409 在前端以 sticky 通知呈现;VERSION_CONFLICT 提供 Reload,VERSION_LEDGER_PENDING 提供 Repair / Abandon;reason 必填以 HTTP 400 + InvalidArgument 返回;FEATURE_DISABLED 关停为 HTTP 503。
  1. 跨规则类型一致:Condition / Tag / Dynamic Config 三类规则共享同一套 ledger、API、UI 组件。
  1. 上游回声不冗余:(content_hash, operation) 在 store 内部去重,Bootstrap 后注册中心 echo 不产生重复行;DELETE 与非 DELETE 不互相折叠;admin / rollback 写入通过 intent 与上游回声关联,避免重复入账。
  1. 代码审查决策矩阵公开:多轮 owner review 后,旧 hint/coalesce 叙述已替换为 intent 驱动实现;关键修复包括删除 dead hint API、补齐 pending intent 修复/放弃、修复 bootstrap 并发 meta 插入、subscriber 同步无损入账、reason 长度校验与 Monaco model 释放。
  1. 不在范围:AffinityRoute、Nacos/Apollo rule subscriber source context、两阶段审批工作流、被 trim 行的软删除留存、大规模 bootstrap batch 优化。其中 AffinityRoute 由我负责后续跟进,其余暂无 owner。
本次测试覆盖功能正确性、并发边界、错误响应与代码质量四个维度,全部测试项通过。
回到首页