type
Post
status
Published
date
May 22, 2026
slug
dubbo-admin-reviewer-report
summary
tags
推荐
category
开源贡献
icon
password
日期: 2026-05-22
Issue: apache/dubbo-admin#1473
版本信息
组件 | 版本 |
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 Route、Tag Route、Dynamic 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,再扫描所有现存规则。RecordBootstrap 对在 rule_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_PENDING或VERSION_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_CONFLICT 与 VERSION_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 | 死码 | AdminHintRegistry、PutAdminHint、RecordMutation 等旧 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 描述与真实实现不一致。修复:删除
AdminHintRegistry、PutAdminHint、RecordMutation 等旧 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::PutConfiguratorWithRuleName 不 return | 不是本 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 | mysql 或 postgres | 生产请用持久化数据库;未配置数据库连接时组件会退回 memory store,仅适合开发/测试 |
expectedVersionId 在所有 mutation 端点(PUT/POST/DELETE)与 /rollback 上都是可选:不传 → 行为完全不变;传 → 触发乐观版本检查。当前检查会先看 open intent,再看 rule_version_meta.current_version,因此可能返回 VERSION_LEDGER_PENDING 或 VERSION_CONFLICT。9. 结论
- 版本账本核心功能完整:Bootstrap、Admin、Upstream、Rollback 四类来源全部正确入册,
version_no单调且不重用。
- 并发与边界保护:乐观锁 409 在前端以 sticky 通知呈现;
VERSION_CONFLICT提供 Reload,VERSION_LEDGER_PENDING提供 Repair / Abandon;reason 必填以 HTTP 400 +InvalidArgument返回;FEATURE_DISABLED关停为 HTTP 503。
- 跨规则类型一致:Condition / Tag / Dynamic Config 三类规则共享同一套 ledger、API、UI 组件。
- 上游回声不冗余:
(content_hash, operation)在 store 内部去重,Bootstrap 后注册中心 echo 不产生重复行;DELETE 与非 DELETE 不互相折叠;admin / rollback 写入通过 intent 与上游回声关联,避免重复入账。
- 代码审查决策矩阵公开:多轮 owner review 后,旧 hint/coalesce 叙述已替换为 intent 驱动实现;关键修复包括删除 dead hint API、补齐 pending intent 修复/放弃、修复 bootstrap 并发 meta 插入、subscriber 同步无损入账、reason 长度校验与 Monaco model 释放。
- 不在范围:AffinityRoute、Nacos/Apollo rule subscriber source context、两阶段审批工作流、被 trim 行的软删除留存、大规模 bootstrap batch 优化。其中 AffinityRoute 由我负责后续跟进,其余暂无 owner。
本次测试覆盖功能正确性、并发边界、错误响应与代码质量四个维度,全部测试项通过。
分享
