资讯动态

TiDB 受限只读模式 `tidb_restricted_read_only` 设计解析与实现深入

发布时间:2026/9/10 21:20:26 来源:尧图企业网站定制
TiDB 受限只读模式tidb_restricted_read_only设计解析与实现深入【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidbtidb_restricted_read_only是 TiDB 提供的一个全局Global 级开关开启后整个集群会最终进入只读状态且对所有用户生效——包括拥有SUPER、CONNECTION_ADMIN等高级权限的用户。本文以 docs/design/2021-06-23-restricted-read-only.md 设计文档为主体结合当前仓库中规划器planner、会话session、系统变量sysvar与权限系统的实现源码完整讲解该开关的设计动机、开启条件、SQL 拦截机制、特权管理与实际使用与验证方式。读完本文你将理解 TiDB 的“受限只读”与 MySQLread_only/super_read_only的本质差异掌握如何安全地让集群进入全局只读以及如何为复制链路单独开放写权限。设计动机为什么需要“面向所有人”的最终只读设计文档开篇即点明核心目标引入TIDB_RESTRICTED_READ_ONLY全局变量开启后集群对所有用户最终只读包括拥有SUPER或CONNECTION_ADMIN权限的用户。这对应的是云上管理员cloud admin在迁移、容灾或维护场景下需要一把“能压过所有普通权限”的总闸。在引入它之前TiDB 虽然注册了read_only与super_read_only两个全局变量但它们并没有真正执行操作在 2021 年设计文档写作时的状态。设计上刻意不把TIDB_RESTRICTED_READ_ONLY构建在 MySQL 语义的 read-only / super-read-only 之上原因是 MySQL 语义会在存在表锁或事务正在提交等情况下阻塞或失败而受限只读需要的是“更宽松的、始终立即返回成功、最终达成”的只读二者行为模型不兼容因此这两组变量互不干扰。设计文档同时给出了三条最核心的设计决策新建全局变量TIDB_RESTRICTED_READ_ONLY其开关打开/关闭只能在安全增强模式SEM下进行且只有拥有RESTRICTED_VARIABLES_ADMIN权限的用户才能修改。引入新的动态权限级别RESTRICTED_REPLICA_WRITER_ADMIN拥有该权限的用户典型如复制/同步服务账号在只读检查中被放行从而保证复制链路仍能写入。变量在某台 TiDB Server 上变更后会通过 PD 广播到所有其他 TiDB Server。正常情况下其他 TiDB 会立刻感知但在某些场景如某台 TiDB 与 PD 断连下延迟最长可达约 30 秒——这正是“最终只读eventually read-only”的含义。变量定义与开关语义变量定义与默认值在当前代码中变量以常量形式注册在 pkg/sessionctx/vardef/tidb_vars.go// TiDBRestrictedReadOnly is meant for the cloud admin to toggle the cluster read only TiDBRestrictedReadOnly tidb_restricted_read_only同名同文件中定义了默认值与运行时状态pkg/sessionctx/vardef/tidb_vars.go 与 pkg/sessionctx/vardef/tidb_vars.goDefTiDBRestrictedReadOnly false // ... RestrictedReadOnly atomic.NewBool(DefTiDBRestrictedReadOnly)注意运行时状态使用原子布尔量atomic.Bool因为该值会被所有 TiDB Server 上的每条 SQL 在规划阶段并发读取必须保证无锁可见性。系统变量注册与“隐藏变量”机制变量在 pkg/sessionctx/variable/sysvar.go 中注册为ScopeGlobal布尔类型变量。其SetGlobal回调显示了一个重要联动行为SetGlobal: func(_ context.Context, s *SessionVars, val string) error { on : TiDBOptOn(val) // For user initiated SET GLOBAL, also change the value of TiDBSuperReadOnly if on s.StmtCtx.StmtType Set { err : s.GlobalVarsAccessor.SetGlobalSysVarOnly(context.Background(), vardef.TiDBSuperReadOnly, ON, false) ... } vardef.RestrictedReadOnly.Store(on) return nil }也就是说当普通用户通过SET GLOBAL打开tidb_restricted_read_only时TiDB 会连带把tidb_super_read_only也置为ON用户主动执行的SET语句场景。反向的tidb_super_read_only变量注册中同文件 pkg/sessionctx/variable/sysvar.go带有一致性校验if TiDBOptOn(result) { return normalizedValue, fmt.Errorf(cant turn off %s when %s is on, vardef.TiDBSuperReadOnly, vardef.TiDBRestrictedReadOnly) }即在tidb_restricted_read_only仍为ON时禁止把tidb_super_read_only关掉防止绕过上层只读约束。变量同时被列入“隐藏变量”清单在 pkg/util/sem/sem.go 的IsInvisibleSysVar中包含了TiDBRestrictedReadOnly对应设计文档要求的“仅在 SEM 模式安全增强模式下、由RESTRICTED_VARIABLES_ADMIN持有者修改”的约束——普通用户在 SEM 开启时甚至无法感知该变量的存在。如何开启操作步骤与前置条件综合设计文档与仓库测试tests/readonlytest/readonly_test.go典型开启流程如下-- 只有具备足够权限的管理员才能执行 SET GLOBAL tidb_restricted_read_only ON;开启后tidb_restricted_read_only与tidb_super_read_only均变为ON前者打开会联动后者未获豁免的用户执行INSERT、UPDATE、DELETE、CREATE TABLE等写操作时会收到错误Error 1836: Running in read-only mode对应 errors.toml 中的planner:1836在tidb_restricted_read_only仍为ON时尝试关闭tidb_super_read_only会收到错误Error 1105: cant turn off tidb_super_read_only when tidb_restricted_read_only is on。关闭时需要注意顺序与联动行为测试用例 tests/readonlytest/readonly_test.go 完整验证了这一点-- 关闭受限只读只关 restricted不影响 super_read_only SET GLOBAL tidb_restricted_read_only OFF; -- 之后 tidb_super_read_only 仍为 ON需要单独再关 SET GLOBAL tidb_super_read_only OFF;即“打开 restricted 会连带打开 super但关闭 restricted不会自动关闭 super”二者必须分开操作避免误放写流量。只读检查的实现在哪拦截、如何拦截设计文档把 SQL 限制收敛在规划planning阶段并给出了一套明确的判定规则。当前源码的实现落在规划入口 pkg/planner/optimize.gofunc Optimize(ctx context.Context, sctx sessionctx.Context, node *resolve.NodeW, is infoschema.InfoSchema) (plan base.Plan, slice types.NameSlice, retErr error) { ... if !sessVars.InRestrictedSQL (vardef.RestrictedReadOnly.Load() || vardef.VarTiDBSuperReadOnly.Load()) { allowed, err : allowInReadOnlyMode(pctx, node.Node) if err ! nil { return nil, nil, err } if !allowed { return nil, nil, errors.Trace(plannererrors.ErrSQLInReadOnlyMode) } }两个前提条件值得注意!sessVars.InRestrictedSQL内部 SQL例如统计信息收集、系统内部维护任务不受只读限制这对应设计文档的“不限制内部 SQL”规则。该路径在 tests/readonlytest/readonly_test.go 的TestInternalSQL中被验证在只读开启后通过内部执行通道写入mysql.stats_top_n仍可成功。只读状态同时读取RestrictedReadOnly与VarTiDBSuperReadOnly两个原子量任何一个为开即进入只读判定。allowInReadOnlyMode允许清单 兜底判断真正执行判定的是 pkg/planner/optimize.go 的allowInReadOnlyMode其流程与设计文档的白名单一一对应func allowInReadOnlyMode(sctx planctx.PlanContext, node ast.Node) (bool, error) { pm : privilege.GetPrivilegeManager(sctx) if pm nil { return true, nil } roles : sctx.GetSessionVars().ActiveRoles // allow replication thread // NOTE: ... 即使 SEM 未开启只有显式授予 RESTRICTED_REPLICA_WRITER_ADMIN 的用户才能绕过 if pm.HasExplicitlyGrantedDynamicPrivilege(roles, RESTRICTED_REPLICA_WRITER_ADMIN, false) { return true, nil } switch node.(type) { case *ast.SetStmt, // 允许改变量否则无法解除只读 *ast.AnalyzeTableStmt, // 允许 ANALYZE TABLE *ast.UseStmt, *ast.ShowStmt, // 允许 SHOW *ast.CreateBindingStmt, // 允许创建 SQL Binding *ast.DropBindingStmt, // 允许删除 SQL Binding *ast.PrepareStmt, // 允许 PREPARE *ast.BeginStmt, // 允许 BEGIN *ast.RollbackStmt: // 允许 ROLLBACK return true, nil case *ast.CommitStmt: txn, err : sctx.Txn(true) ... if !txn.IsReadOnly() { return false, txn.Rollback() // 事务有写入则中止 } return true, nil } ... return core.IsReadOnlyInternal(node, vars, false), nil }对照设计文档可以逐条印证白名单规则设计文档规则源码中的 AST 类型允许 set 变量否则无法解除只读*ast.SetStmt允许analyze table*ast.AnalyzeTableStmt允许show*ast.ShowStmt允许 create / drop SQL bindings*ast.CreateBindingStmt/*ast.DropBindingStmt允许 prepare SQL*ast.PrepareStmt允许begin和rollback*ast.BeginStmt/*ast.RollbackStmtcommit仅在事务无变更时允许否则事务中止*ast.CommitStmt分支txn.IsReadOnly()为假则回滚并拒绝其余情况交给只读判定core.IsReadOnlyInternal(node, vars, false)最后一个分支的core.IsReadOnlyInternal是“兜底”判定负责EXPLAIN、EXECUTE等需要递归分析语句的场景。它定义在 pkg/planner/core/util.go对ExecuteStmt会先反查出被PREPARE的原始语句再做只读判断最终调用 AST 层的ast.IsReadOnlyfunc IsReadOnlyInternal(node ast.Node, vars *variable.SessionVars, checkGlobalVars bool) bool { if execStmt, isExecStmt : node.(*ast.ExecuteStmt); isExecStmt { prepareStmt, err : GetPreparedStmt(execStmt, vars) ... return ast.IsReadOnly(prepareStmt.PreparedAst.Stmt, checkGlobalVars) } return ast.IsReadOnly(node, checkGlobalVars) }注意这里传参为checkGlobalVars false源码注释为 “Passing false allows global variables updates in read-only mode”即兜底判定对“修改全局变量的语句”采取宽容处理避免把关闭只读的操作自身也拦掉。提交阶段二次校验堵住“长事务”漏洞仅有规划阶段的拦截并不足够一条已经通过规划、正在执行的长事务例如长时间运行的 auto-commit 语句可能在只读打开后才尝试提交。为此会话提交入口 pkg/session/session.go 的doCommit中还有一次提交前复检// check if the cluster is read-only if !s.sessionVars.InRestrictedSQL (vardef.RestrictedReadOnly.Load() || vardef.VarTiDBSuperReadOnly.Load()) { pm : privilege.GetPrivilegeManager(s) roles : s.sessionVars.ActiveRoles if pm ! nil !pm.HasExplicitlyGrantedDynamicPrivilege(roles, RESTRICTED_REPLICA_WRITER_ADMIN, false) { s.RollbackTxn(ctx) return plannererrors.ErrSQLInReadOnlyMode } }该复检与规划阶段使用完全相同的判定依据RestrictedReadOnly/VarTiDBSuperReadOnlyRESTRICTED_REPLICA_WRITER_ADMIN豁免发现写事务则直接回滚并返回Error 1836从执行路径上杜绝了“漏网之鱼”。测试 tests/readonlytest/readonly_test.go 的TestRestrictionWithConnectionPool用持续写入的并发连接验证了开启只读后写请求会以Running in read-only mode迅速终止。权限管理谁可以写、谁可以改开关设计文档在权限管理上给出了两个明确结论当前实现与其一致RESTRICTED_REPLICA_WRITER_ADMIN复制写入豁免动态权限RESTRICTED_REPLICA_WRITER_ADMIN注册于 pkg/privilege/privileges/privileges.goRESTRICTED_REPLICA_WRITER_ADMIN, // Can write to the sever even when tidb_restriced_read_only is turned on.其最关键的语义设计文档特别强调源码注释也再次重申即使 SEM 没有开启SUPER用户也不会自动获得该权限——SUPER在非 SEM 模式下虽然拥有全部权限但代码中使用的HasExplicitlyGrantedDynamicPrivilege只认“显式授予”因此复制账号必须被显式 GRANT 才能写入-- 测试与使用示例见 pkg/session/test/session_test.go 与 tests/readonlytest/readonly_test.go GRANT RESTRICTED_REPLICA_WRITER_ADMIN ON *.* TO replication_user%;设计文档同时记录了一个被否定的备选方案新增独立的RESTRICTED_READ_ONLY_ADMIN权限让持有者既能改开关又能写入——但因为这会引入冗余的新权限层级而被否决最终选择复用既有的RESTRICTED_VARIABLES_ADMIN来管控开关本身。RESTRICTED_VARIABLES_ADMIN管控开关修改tidb_restricted_read_only需要RESTRICTED_VARIABLES_ADMIN并在 SEM 模式下。对不具备相应权限的普通账号执行SET GLOBAL会得到典型错误Error 1227: Access denied; you need (at least one of) the SUPER or SYSTEM_VARIABLES_ADMIN privilege(s) for this operation相关断言见 tests/readonlytest/readonly_test.go。而拥有RESTRICTED_REPLICA_WRITER_ADMIN的复制账号也只能写入数据依然不能修改只读开关——测试 tests/readonlytest/readonly_test.go 验证了授予复制写权限的账号尝试关闭开关同样会被拒绝。组合效果账号类型只读开启后写数据修改tidb_restricted_read_only普通用户❌ 拒绝Error 1836❌ 拒绝Error 1227SUPER/CONNECTION_ADMIN❌ 拒绝除非显式豁免需 SEM RESTRICTED_VARIABLES_ADMINRESTRICTED_REPLICA_WRITER_ADMIN持有者✅ 允许❌ 拒绝内部 SQL如统计信息写入✅ 允许InRestrictedSQL—测试 tests/readonlytest/readonly_test.go 的TestReplicationWriter完整演示了该矩阵开启只读后SUPER用户写INSERT立即得到Error 1836而持有RESTRICTED_REPLICA_WRITER_ADMIN的账号在同一时间段内持续写入成功。与 MySQLread_only/super_read_only的差异设计文档明确列出与 MySQL 的核心差异——开启语义MySQL 在开启read_only或super_read_only时若存在复制未处理、表锁等场景可能失败或阻塞TiDB 的TIDB_RESTRICTED_READ_ONLY立即返回成功虽然部分 TiDB Server 可能尚未收到变量变更通知、仍短暂处于可写状态但全局变量的变更最终会广播到集群内所有 TiDB Server完成“最终只读”。因此它并不承诺“瞬间全部只读”而是满足云管理场景对“总开关立即下发、最终收敛”的诉求。与这条差异相呼应当前代码将tidb_super_read_only与tidb_restricted_read_only做了联动与互斥校验打开 restricted 自动打开 superrestricted 未关时禁止关闭 super进一步强化了“受限只读是最强约束”的地位。对需要严格 MySQL 兼容语义的用户设计文档的 Alternative 一节也说明原样实现 MySQL 语义、并以其为基础构建受限只读的方案因“只需最终只读、且必须始终成功返回”而被放弃。源码实现地图与可验证入口若想深入阅读或二次验证推荐按以下路径追踪设计文档原文docs/design/2021-06-23-restricted-read-only.md变量常量与运行时原子状态pkg/sessionctx/vardef/tidb_vars.go变量注册、联动与互斥校验pkg/sessionctx/variable/sysvar.goSEM 隐藏变量清单pkg/util/sem/sem.go规划阶段拦截入口与白名单判定pkg/planner/optimize.go、pkg/planner/optimize.go只读语句兜底判定含 EXECUTE 展开pkg/planner/core/util.go提交阶段二次复检pkg/session/session.go动态权限注册pkg/privilege/privileges/privileges.go错误文案定义errors.toml集成测试真实双 TiDB 节点场景tests/readonlytest/readonly_test.go、tests/readonlytest/README 所属测试框架单元/会话层相关用例见 pkg/session/test/session_test.go 与 pkg/session/test/txn/txn_test.go。小结tidb_restricted_read_only以“面向所有用户、含SUPER/CONNECTION_ADMIN、最终生效”的方式为 TiDB 集群提供了一把由云端管理员掌握的总写开关。它不追求 MySQL 式“开即严格阻塞”的瞬时语义而是通过“全局变量经 PD 广播 → 各 TiDB 在规划阶段按白名单拦截 → 提交阶段二次复检防长事务绕过 →RESTRICTED_REPLICA_WRITER_ADMIN显式豁免复制写入”的多层设计在即时成功下发与最终严格只读之间取得平衡。无论是要为集群做一致性快照、计划内维护还是要保障基于日志/CDC 的复制链路持续写入理解这一变量的语义边界允许列表、权限要求、与super_read_only的联动、约 30 秒的最终一致窗口都是正确使用它的前提。【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

免费获取报价