资讯动态

Bytebase 统一实例许可证(Unified Instance License)设计解析:从双字段到单一实例数的演进路径

发布时间:2026/9/15 12:05:51 来源:尧图企业网站定制
Bytebase 统一实例许可证Unified Instance License设计解析从双字段到单一实例数的演进路径【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase导读Bytebase 的许可证License自诞生起便携带两个实例相关字段注册实例上限instance与已激活实例上限instanceCount分别控制能接入多少个实例与其中多少个能使用按实例计费的付费功能。随着产品演进Bytebase 希望将这种双数体验收敛为一个数字——当许可证的有效注册上限 ≤ 有效激活上限时所有已注册实例都应被视为已激活无需再暴露任何分配Assignment步骤。本文基于 统一实例许可证设计文档结合backend/enterprise/license.go、backend/api/v1/instance_service.go、backend/api/v1/actuator_service.go及前端 store 源码完整讲解统一模式的判定规则、后端行为、前端展示、许可证签发策略与分阶段实施路径帮助读者理解 Bytebase 如何在不动数据库 Schema、不改 Proto、不改 JWT 结构的前提下实现产品与销售的简化。一、背景为什么会有两个实例数字1.1 许可证的既有双字段结构Bytebase 的企业许可证是一枚由 RSA 私钥签名的 JWT签发者恒为bytebase受众恒为bb.license其 Claims 定义在 backend/enterprise/license.gotype Claims struct { ActiveInstances int json:instanceCount Instances int json:instance Seats int json:seat HA bool json:ha Trialing bool json:trialing Plan string json:plan OrgName string json:orgName WorkspaceID string json:workspaceId jwt.RegisteredClaims }其中Claims.Instances/ JWTinstance注册上限registration cap决定工作区最多能接入多少个实例。Claims.ActiveInstances/ JWTinstanceCount激活上限activated cap决定其中多少个实例能启用按实例计费的付费能力。与此同时运行时还会在**实例元数据instance metadata**中按实例记录激活状态Activation布尔值。多类按实例计费的付费功能——包括数据脱敏data masking、只读连接read-only connections、自定义同步时间custom sync time、外部密钥管理器external secret manager——都会先检查实例的激活状态只有激活的实例才能使用这些功能。1.2 长期目标与第一阶段约束长期目标显然是单一实例数字。但设计文档明确要求第一阶段不得做破坏性变更不合并/不删除既有 Schema 字段不迁移存量客户数据不改 Proto 定义不改 JWT 解析逻辑不破坏已上传许可证的兼容性。Phase 1 的做法是识别有效注册上限 ≤ 有效激活上限的许可证并让它们在行为上表现得像一个单数许可证——即所有已注册实例都视为已激活但底层存储的激活元数据保持不变。二、统一实例许可证的产品契约2.1 统一的判定条件设计文档给出的定义是effective registration cap effective activated cap即有效注册上限小于等于有效激活上限时进入统一实例许可证模式unified instance license mode。2.2 必须使用有效限制而非原始 JWT 值关键的工程细节是比较必须走既有的有效限制语义effective limit semantics不能拿 JWT 里的原始数值直接比较。原因是 Bytebase 的许可证存在plan 兜底与无限值unlimited的 fallback 规则后端注册上限LicenseService.GetInstanceLimit(ctx, workspaceID)后端激活上限LicenseService.GetActivatedInstanceLimit(ctx, workspaceID)前端注册上限subscriptionStore.instanceCountLimit前端激活上限subscriptionStore.instanceLicenseCount。例如以下两种边界情形场景判定注册上限无限unlimited、激活上限无限统一unified注册上限无限、激活上限仅 20遗留拆分legacy split-cap也就是说无限注册 无限激活应被判定为统一而无限注册 有限激活仍属于拆分模式因为激活上限比注册上限小。2.3 统一模式下的行为承诺每个已注册实例都被视为有效激活effectively activated不修改存储的实例元数据stored instance metadata 不被 mutation用户侧不展示任何面向分配的入口如Assign License按钮如果工作区之后换回拆分式许可证存储的激活状态将重新生效——这保证了可逆性不会因统一模式的假激活污染底层数据。2.4 拆分模式保持不变在遗留拆分模式下现有运行时行为、既有 UI 与文案全部保留客户仍然可以自主选择哪些实例启用按实例计费的付费功能。三、后端行为与源码实现3.1 集中式判定 Helper设计文档要求在LicenseService上新增集中式 helper该实现已落地于 backend/enterprise/license.gofunc isUnifiedInstanceLimit(instanceLimit, activatedInstanceLimit int) bool { return instanceLimit activatedInstanceLimit } // IsUnifiedInstanceLicense returns whether every registrable instance is effectively activated. func (s *LicenseService) IsUnifiedInstanceLicense(ctx context.Context, workspaceID string) bool { return isUnifiedInstanceLimit( s.GetInstanceLimit(ctx, workspaceID), s.GetActivatedInstanceLimit(ctx, workspaceID), ) }其中两个 limit 函数的语义如下同一文件 license.goGetInstanceLimit优先取许可证subscription.Instances为 0 时回退到 plan 配置的maximumInstanceCount若 plan 值为-1Enterprise则视为math.MaxInt无限。GetActivatedInstanceLimit取subscription.ActiveInstances为负值时视为math.MaxInt无限。正是这两层 fallback保证了零值与无限值能与既有的 plan/license 规则一致从而正确处理上表所述边界情形。3.2 实例级功能开关Feature Gates设计文档要求IsFeatureEnabledForInstance仍然先做 plan 权益检查再决定是否检查实例的存储激活状态。落地实现见 license.gofunc (s *LicenseService) IsFeatureEnabledForInstance(ctx context.Context, workspaceID string, f v1pb.PlanFeature, instance *store.InstanceMessage) error { plan : s.GetEffectivePlan(ctx, workspaceID) // DO NOT check instance license for FREE plan. if plan v1pb.PlanType_FREE { return s.IsFeatureEnabled(ctx, workspaceID, f) } if err : s.IsFeatureEnabled(ctx, workspaceID, f); err ! nil { return err } if !s.IsInstanceEffectivelyActivated(ctx, workspaceID, instance) { ... return errors.Errorf(feature %s requires an activated instance under the current license: %s, f.String(), instanceID) } return nil }而是否有效激活的判定被收敛到 IsInstanceEffectivelyActivatedfunc (s *LicenseService) IsInstanceEffectivelyActivated(ctx context.Context, workspaceID string, instance *store.InstanceMessage) bool { if instance nil { return false } return instance.Metadata.GetActivation() || s.IsUnifiedInstanceLicense(ctx, workspaceID) }可见其逻辑与设计完全一致存储激活为 true或处于统一许可证模式两者任一成立即视为有效激活。在统一模式下即使instance.Metadata.GetActivation() false也能通过功能检查在拆分模式下则保留原有必须显式激活的错误行为。3.3 实例 API 响应设计文档要求统一模式下公开的 v1 实例响应应返回activationtrue同时存储元数据保持不变拆分模式返回存储的激活状态。实现位于 backend/api/v1/instance_service.goListInstances 与 GetInstance 均复用同一转换逻辑见同文件 L551workspaceID : common.GetWorkspaceIDFromContext(ctx) for _, instance : range instances { ins : convertToV1Instance(instance, s.licenseService.IsInstanceEffectivelyActivated(ctx, workspaceID, instance)) response.Instances append(response.Instances, ins) }即响应层的activation字段由IsInstanceEffectivelyActivated计算得出不会回写数据库。3.4 Actuator 统计设计文档要求统一模式下 Actuator 返回activatedInstanceCount totalInstanceCount。实现见 backend/api/v1/actuator_service.goactiveInstanceCount, err : s.store.CountActiveInstances(ctx, workspaceID) ... serverInfo.TotalInstanceCount int32(activeInstanceCount) if s.licenseService.IsUnifiedInstanceLicense(ctx, workspaceID) { serverInfo.ActivatedInstanceCount int32(activeInstanceCount) } else { activatedInstanceCount, err : s.store.GetActivatedInstanceCount(ctx, workspaceID) ... serverInfo.ActivatedInstanceCount int32(activatedInstanceCount) }拆分模式下仍走store.GetActivatedInstanceCount统计真实激活的实例数。3.5 激活配额检查Activation Quota Checks设计文档要求统一模式下跳过专门守卫开启 activation的配额检查实例创建仍受GetInstanceLimit的注册上限保护。实现见 instance_service.goconst instanceExceededError activation instance count has reached the limit (%v) func (s *InstanceService) checkActivationLimit(ctx context.Context, workspaceID string, activating bool) error { if !activating || s.licenseService.IsUnifiedInstanceLicense(ctx, workspaceID) { return nil } activatedInstanceLimit : s.licenseService.GetActivatedInstanceLimit(ctx, workspaceID) count, err : s.store.GetActivatedInstanceCount(ctx, workspaceID) if err ! nil { return connect.NewError(connect.CodeInternal, err) } if count activatedInstanceLimit { return connect.NewError(connect.CodeResourceExhausted, errors.Errorf(instanceExceededError, activatedInstanceLimit)) } return nil }该函数在实例创建L421、实例更新开启激活L947、以及数据源相关路径L1066等位置被调用统一模式下会直接放行。3.6 本阶段不动的部分设计文档强调Phase 1 不修改数据库 Schema、不新增迁移文件、不改 Proto 定义、不改 JWT 解析、不破坏已上传许可证兼容性。从上述源码可以看出所有行为变化都是读取路径上的叠加判断未触碰存储与协议。四、前端行为与源码实现4.1 展示模式的计算设计文档要求前端基于有效 store 值计算展示模式而非比较原始subscription.instances与subscription.activeInstances。落地实现见 frontend/src/stores/app/workspace.tsinstanceCountLimit: () { const subscription get().subscription; const licenseLimit subscription?.instances ?? 0; if (licenseLimit 0) { return licenseLimit; } const planLimit PLANS.find((plan) plan.type get().currentPlan()) ?.maximumInstanceCount ?? 0; if (planLimit 0) { return licenseLimit 0 ? licenseLimit : Number.MAX_VALUE; } return planLimit; }, instanceLicenseCount: () { const count get().subscription?.activeInstances ?? 0; return count 0 ? Number.MAX_VALUE : count; }, hasUnifiedInstanceLicense: () { return get().instanceCountLimit() get().instanceLicenseCount(); },hasUnifiedInstanceLicense的判定逻辑与后端IsUnifiedInstanceLicense完全对称同样正确处理了许可证未显式设置时按 plan 兜底、负数代表无限等有效限制语义。4.2 统一模式下的 UI 收敛设计文档要求统一模式下隐藏面向分配的入口Assign License按钮、分配表单触发器、缺许可证 CTA不再把 Assigned / Total Instance License 作为主订阅指标改为基于instanceCountLimit的单一实例配额实例表单中禁用/隐藏激活开关因为激活已恒为 true本地功能守卫辅助函数在plan 已含该功能 许可证统一时不展示缺许可证状态。对应的源码佐证frontend/src/stores/app/workspace.ts 中hasInstanceFeature与instanceMissingLicense都把hasUnifiedInstanceLicense()纳入判定统一模式下不报告缺许可证hasInstanceFeature: (feature, instance) { ... return checkInstanceFeature( plan, feature, get().hasUnifiedInstanceLicense() || instance.activation ); }, instanceMissingLicense: (feature, instance) { if (!instanceLimitFeature.has(feature) || !instance) { return false; } if (get().hasUnifiedInstanceLicense()) { return false; } return get().hasFeature(feature) !instance.activation; },frontend/src/components/FeatureAttention.tsx 使用hasUnifiedInstanceLicense()决定是否展示功能缺失警示frontend/src/components/instance/InstanceFormBody.tsx 在实例表单中依据hasUnifiedInstanceLicense()决定是否展示激活开关L1309 附近frontend/src/routes/workspace/SubscriptionPage.tsx 在统一模式下不展示分配入口并据此计算展示指标。4.3 拆分模式不变拆分模式下前端保留既有 UI 与文案分配表单、激活开关、缺许可证警告、activated/total 指标全部照旧且本阶段不重命名、不重设计遗留分配工作流。设计文档同时强调后端行为始终是权威authoritative前端模式检测仅用于展示层目的是避免呈现误导性的分配 UI。五、许可证签发与遗留处理5.1 Schema 保持不变许可证 Claims 结构不变ActiveInstances int json:instanceCount Instances int json:instance5.2 新许可证策略等额签发新 SaaS 许可证应签发等额的注册上限与激活上限CreateLicense保持现状从同一个LicenseParams.Instances同时设置两个 claims增加回归测试防止后续 SaaS 许可证生成时两个 claims 意外分叉手工及自托管self-host许可证生成工具/流程也应签发等额 caps。源码层面newLicenseClaims 正是这一策略的实现func newLicenseClaims(params *LicenseParams) *Claims { return Claims{ Plan: params.Plan, Seats: params.Seats, ActiveInstances: params.Instances, Instances: params.Instances, WorkspaceID: params.WorkspaceID, Trialing: params.Trialing, } }CreateLicenselicense.go仅在配置了私钥SaaS 模式时可用负责填充 issuer/audience/kid 并完成 RS256 签名。5.3 遗留许可证处理原则存量与已上传的不等额许可证继续有效若有效注册上限 有效激活上限Bytebase 保持遗留拆分行为不自动变更实例激活状态不自动缩水、也不自动扩容客户权益续约或迁移到等额许可证走正常商务/账号流程。5.4 生效上限的兼容性细节值得注意设计文档给出的统一判定用的是有效上限而非原始 JWT 数值这带来一个微妙而正确的行为——若某个遗留许可证的instanceCount大于等于instance例如手动签发的 20 注册 / 无限激活或 20/20 等额它同样会被识别为统一模式。这意味着统一并非只针对新签发的等额许可证而是对任何满足有效上限比较关系的许可证都成立。六、分阶段实施路径设计文档将整体演进划分为四个阶段并明确本文覆盖 Phase 1、记录后续方向阶段内容范围Phase 1有效统一模式新增归一化后端 helper在功能开关、实例 API 响应、Actuator 统计、激活配额检查中应用有效激活行为更新前端展示避免分配 UI保持 Schema、存储元数据、已上传许可证与拆分行为兼容Phase 2签发策略加固确保所有受支持的新许可证签发路径都产生等额 caps补充/更新自托管许可证的手工生成流程文档继续接受遗留不等额许可证签发侧Phase 3遗留清理识别仍使用拆分许可证的工作区/账号在续约或显式账号操作时迁移避免静默缩水与静默免费扩容Phase 4单数清理拆分许可证全部退场后将产品与代码概念收敛为单一实例数移除分配 UI 与基于激活的功能门控兼容窗口后弃用/忽略旧 JWT 字段终极形态Phase 2 至 Phase 4 需要另行规划本设计不做展开。七、测试策略设计文档给出了前后端测试覆盖清单仓库中已有对应测试落地于 backend/enterprise/license_test.go7.1 后端测试TestIsUnifiedInstanceLimitL33-L54覆盖等额有限 caps10/10 → true、激活多于注册10/20 → true、拆分50/20 → false、双侧无限MaxInt/MaxInt → true、无限注册有限激活MaxInt/20 → false、有限注册无限激活20/MaxInt → true。测试直接驱动纯函数isUnifiedInstanceLimit这正是设计文档等额、激活多于注册、无限/无限返回 true拆分返回 false的自动化印证。TestIsFeatureEnabledForInstanceUnifiedLicenseL56-L72统一模式下即使实例存储Activation: falseFEATURE_DATA_MASKING等功能检查也应通过。TestIsFeatureEnabledForInstanceSplitLicenseL74-L90拆分模式下存储Activation: false的实例在同样的功能检查下仍应失败。TestIsInstanceEffectivelyActivatedL92-L122统一许可证有效激活存储未激活实例拆分许可证沿用存储激活状态false 不通过、true 通过。TestCreateLicenseUsesEqualInstanceClaimsL124-L137newLicenseClaims从同一Instances参数生成等额的Instances与ActiveInstances即 SaaS 签发等额 claims 的回归防线。设计文档还要求覆盖实例 API 响应在统一模式下返回activationtrue且不修改存储元数据Actuator 返回activatedInstanceCount totalInstanceCount激活配额检查仅在统一模式下被跳过SaaSCreateLicense输出等额instance/instanceCountclaims。7.2 前端测试设计文档要求覆盖统一模式由有效值计算得出而非原始订阅字段统一模式下不出现缺许可证警告统一模式下隐藏分配入口统一模式下实例表单不暴露可操作的激活开关拆分模式保留既有分配 UI。仓库中 frontend/src/stores/app/index.test.ts 已有对instanceCountLimit、instanceLicenseCount、hasUnifiedInstanceLicense有效值语义的断言frontend/src/components/FeatureAttention.test.tsx 与 frontend/src/routes/workspace/SubscriptionPage.test.tsx 则覆盖了组件层面的展示行为。7.3 最终验证设计文档要求最终实现验证遵循仓库 AGENTS.md 约定对修改文件运行相关的 Go 格式化、lint、测试、构建以及前端的 fix/check/type-check/test 命令。八、总结一个数字两种兼容Bytebase 的 Unified Instance License 设计展示了典型的渐进式简化工程思路在产品契约层先把两个数字相等的情形定义为一个数字在行为层通过集中式 helperIsUnifiedInstanceLicense叠加有效激活语义在存储与协议层保持零变更。这样既能让新客户立即获得无分配步骤的极简体验又能让存量拆分许可证客户继续按既有方式工作并在后续阶段通过签发策略加固、遗留清理、单数收敛逐步完成真正的架构简化。对于想要深入代码的读者建议按以下路径阅读许可证 Claims 与有效限制解析backend/enterprise/license.go、backend/enterprise/license.go实例 API 响应与激活配额检查backend/api/v1/instance_service.goActuator 统计backend/api/v1/actuator_service.go前端有效值计算与统一判定frontend/src/stores/app/workspace.ts后端回归测试backend/enterprise/license_test.go【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价