TiDB 日志脱敏机制全解析:tidb_redact_log 系统变量与底层实现
2026/9/10 19:41:38 网站建设 项目流程

TiDB 日志脱敏机制全解析:tidb_redact_log 系统变量与底层实现

【免费下载链接】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

导读

数据库常存储身份证号、信用卡号等敏感信息,这些信息可能以键值对等形式出现在错误消息里,并随错误日志一起被打印、传输,造成敏感数据泄露风险。TiDB 为此提供了tidb_redact_log系统变量开关,可对日志与错误消息中的敏感内容进行统一脱敏。本文基于 pkg/errno/logredaction.md 的设计说明,结合当前仓库的 系统变量定义 与 脱敏工具实现,完整讲解该功能的三种工作模式、实战开启方法、底层原理与日志反脱敏处理方案。

一、问题背景:敏感信息为何会进入日志

数据库系统中存储的数据往往包含敏感信息(如客户身份证号、信用卡号等)。在某些场景下,这些敏感信息会以键值对等形式出现在错误消息中,例如唯一键冲突错误会直接打印导致冲突的具体值:

mysql> create table t (a int, unique key (a)); Query OK, 0 rows affected (0.00 sec) mysql> insert into t values (1),(1); ERROR 1062 (23000): Duplicate entry '1' for key 'a'

上述错误中的'1'就是实际写入的数据值。当这类错误被捕获并记录到日志时,连同执行的 SQL 语句一起,敏感数据就被"顺带"写入了日志文件。TiDB 日志中可能出现的泄露点主要有两类:

  • 错误消息本身:如Duplicate entry '1' for key 'a'直接包含数据值;
  • 执行的 SQL 语句:日志中记录的[sql="..."]字段会还原完整的 SQL 文本,其中可能内嵌用户写入的敏感字面量。

对于不希望敏感信息随日志扩散的用户,TiDB 提供了可配置的开关来隐藏这些可能敏感的信息。

二、核心开关:tidb_redact_log 系统变量

tidb_redact_log是 TiDB 的日志脱敏总开关。在 系统变量定义 中可以看到其完整元信息:

{Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBRedactLog, Value: vardef.DefTiDBRedactLog, Type: vardef.TypeEnum, PossibleValues: []string{vardef.Off, vardef.On, vardef.Marker}, InternalSessionVariable: true, SetGlobal: func(_ context.Context, s *SessionVars, val string) error { s.EnableRedactLog = val // NOTE: switch of errors is a singleton, thus we can not set it for different sessions errors.RedactLogEnabled.Store(val) return nil }, SetSession: func(s *SessionVars, val string) error { s.EnableRedactLog = val return nil }, },

关键信息如下:

属性取值
变量名tidb_redact_log
作用域Global + Session(ScopeGlobal \| ScopeSession
类型枚举(TypeEnum
可选值OFFONMARKER
内部变量是(InternalSessionVariable: true

对应地,会话结构体SessionVars中也有EnableRedactLog字段,见 pkg/sessionctx/variable/session.go#L1469-L1470,注释明确说明其可能取值为'OFF''ON''MARKER'

值得注意的是SetGlobal回调中的一个重要设计:设置全局变量时除了写入会话字段,还会调用errors.RedactLogEnabled.Store(val)更新错误库(pingcap/errors)中的单例开关。源码注释特别指出"errors 的开关是单例,因此无法为不同 session 分别设置"——也就是说,真正控制错误消息脱敏生效的全局开关在SetGlobal时被同步更新,而SetSession只影响会话内的 SQL 语句脱敏展示。

三、三种模式:OFF / ON / MARKER

根据变量定义,tidb_redact_log有三种枚举取值,其行为由 pkg/util/redact/redact.go#L38-L63 中的String()函数统一实现:

// String will redact the input string according to 'mode'. func String(mode string, input string) string { switch mode { case "MARKER": b := &strings.Builder{} b.Grow(len(input)) _, _ = b.WriteRune('‹') for _, c := range input { if c == '‹' || c == '›' { _, _ = b.WriteRune(c) _, _ = b.WriteRune(c) } else { _, _ = b.WriteRune(c) } } _, _ = b.WriteRune('›') return b.String() case "OFF": return input case "ON": return "" default: // should never happen intest.Assert(false, "invalid redact mode") return "" } }

各模式语义如下:

  • OFF(默认关闭):原样返回输入字符串,日志与错误消息保持明文,行为不受影响;
  • ON(完全脱敏):返回空字符串,敏感内容不进入日志。在错误消息场景下,结合 errors 库的脱敏逻辑,实际打印时会被替换为?占位符;
  • MARKER(标记模式):用特殊 Unicode 字符将敏感内容包裹起来,日志中既能看到脱敏位置,又能在事后借助工具恢复或彻底清除这些内容。若内容中本身出现,实现会将其双写以区分于标记边界。

此外 redact.go 还提供了配套的辅助函数:

  • NeedRedact():读取全局errors.RedactLogEnabled单例开关,判断当前是否处于脱敏模式(非Disable且非空即需要脱敏);
  • Value(arg):开启脱敏时统一返回"?",否则返回原始参数;
  • Key(key):开启脱敏时返回"?",否则返回 key 的大写十六进制编码;
  • WriteRedact(builder, v, redact):按模式写字符串——MARKER 写‹v›、Enable 写?、否则原样写入。

四、实战验证:开启前后对比

1. 开启脱敏

沿用原文档 logredaction.md 的示例,执行:

mysql> set @@global.tidb_redact_log=1; Query OK, 0 rows affected (0.00 sec)

说明:在早期版本中该开关以布尔值形式出现,1表示开启。当前仓库的变量定义为枚举类型,可取OFFONMARKER三种值,因此实际使用时应写为set @@global.tidb_redact_log = ON;OFF/MARKER

2. 观察错误消息的变化

开启脱敏后,同样的重复键冲突:

mysql> insert into t values (1),(1); ERROR 1062 (23000): Duplicate entry '?' for key '?'

对比开启前Duplicate entry '1' for key 'a',冲突的具体数据值'1'与索引名'a'都被替换为?占位符,敏感信息不再暴露。

3. 观察日志的变化

对应的 TiDB 日志前后对比如下(原文档示例):

# 开启 tidb_redact_log 之前 [2020/10/20 11:45:37.796 +08:00] [INFO] [conn.go:800] ["command dispatched failed"] [conn=5] [connInfo="id:5, addr:127.0.0.1:57222 status:10, collation:utf8_general_ci, user:root"] [command=Query] [status="inTxn:0, autocommit:1"] [sql="insert into t values (1),(1)"] [txn_mode=OPTIMISTIC] [err="[kv:1062]Duplicate entry '1' for key 'a'"] # 开启 tidb_redact_log 之后 [2020/10/20 11:45:49.539 +08:00] [INFO] [conn.go:800] ["command dispatched failed"] [conn=5] [connInfo="id:5, addr:127.0.0.1:57222 status:10, collation:utf8_general_ci, user:root"] [command=Query] [status="inTxn:0, autocommit:1"] [sql="insert into t values ( ? ) , ( ? )"] [txn_mode=OPTIMISTIC] [err="[kv:1062]Duplicate entry '?' for key '?'"]

可以看到脱敏同时覆盖了两个泄露面:

  • SQL 语句字段[sql="insert into t values (1),(1)"]变为[sql="insert into t values ( ? ) , ( ? )"],SQL 中的数值字面量被?替换;
  • 错误消息字段[err="[kv:1062]Duplicate entry '1' for key 'a'"]变为[err="[kv:1062]Duplicate entry '?' for key '?'"]

也就是说,开启tidb_redact_log后,敏感内容在错误消息和日志两个层面都被统一隐藏,这也是该设计文档标题"Log Redaction"的核心目标。

五、底层原理:错误库单例开关与脱敏链路

1. 全局开关如何生效

从 系统变量定义 可以看到,SetGlobal会把变量值Storeerrors.RedactLogEnabled(来自 pingcap/errors 库的原子单例)。此后 TiDB 在打印错误、记录 SQL 时,会依据这个全局状态决定是输出明文还是?占位符。例如 pkg/util/dbterror/terror_test.go#L40-L51 的测试就展示了如何向errors.RedactLogEnabled写入RedactLogEnable/RedactLogMarker来验证不同模式下的错误输出。

2. 日志与错误的脱敏接入点

从源码结构看,脱敏逻辑被广泛接入日志输出路径:

  • 连接层pkg/server/conn.gopkg/server/conn_stmt.go在打印命令执行失败、语句信息时使用脱敏;
  • 执行层pkg/executor/adapter.go在打印 SQL 与错误时调用脱敏;
  • 表达式层pkg/expression/constant.gopkg/expression/explain.go对常量与执行计划信息进行脱敏处理;
  • 优化器与物理算子pkg/planner/core/operator/physicalop/下多个物理算子(如physical_batch_point_get.gophysical_cte.gophysical_topn.go等)在记录计划相关信息时使用RedactLogEnable/RedactLogMarker相关逻辑。

这种"变量开关 → 全局单例 → 各模块日志打印点"的链路设计,保证了开启后能对全链路日志输出生效,而不必在每个业务模块中单独维护配置。

3. 关键/值的脱敏策略

对于日志中出现的键值对信息,脱敏策略并不相同(见 redact.go 的Key()Value()):

  • Key(键):脱敏时统一替换为?,不脱敏时输出大写十六进制编码;
  • Value(值):脱敏时统一替换为?

这一设计既保证了敏感数据不泄露,也保留了日志中"存在哪些字段"的结构信息,便于问题定位。

六、MARKER 模式与日志反脱敏处理

当设置为MARKER模式时,日志中的敏感内容会被包裹。这种模式适合以下诉求:既要保证日志发布到外部时敏感内容被明确标记出来,又希望在内部排障时能精确恢复或彻底清除标记内容。

pkg/util/redact/redact.go#L79-L182 提供了两个反脱敏入口:

  • DeRedactFile(remove bool, input string, output string):直接处理文件,按行读取输入文件,输出到指定文件(output-时输出到标准输出);
  • DeRedact(remove bool, input io.Reader, output io.Writer, sep string):面向任意 Reader/Writer 的底层实现,按sep(如换行符)逐行处理。

两个函数都逐行扫描‹...›标记:

  • remove = true:将标记内的敏感内容整体替换为一个?,彻底清除敏感信息;
  • remove = false:去除标记,还原出原始明文,供内部可信环境排障使用。

同时,对/被内容本身包含的情况(双写形式)做了兼容解析,避免误判标记边界。配套的单元测试位于 pkg/util/redact/redact_test.go。

七、生态联动:与其他脱敏机制的协同

1. tidb_slow_log_masking 已被移除

TiDB 历史上还有一个慢日志脱敏变量tidb_slow_log_masking。在 pkg/sessionctx/variable/removed.go#L43 中可以看到它已被移入removedSysVars映射,并给出迁移提示:

tiDBSlowLogMasking: "use tidb_redact_log instead",

即:旧有的慢日志脱敏开关统一由tidb_redact_log替代,用户应改用后者完成日志脱敏。这也说明tidb_redact_log是当前仓库中日志脱敏的标准入口。

2. 备份任务信息的凭据脱敏

除了日志与错误消息,redact.go 还定义了TaskInfoRedacted类型,专门用于脱敏备份流任务(backup.StreamBackupTaskInfo)中的存储凭据:

  • S3:AccessKeySecretAccessKeySseKmsKeyId替换为[REDACTED]
  • GCS:CredentialsBlob替换为[REDACTED]
  • Azure Blob:SharedKeyAccessSigEncryptionKey替换为[REDACTED]

可以看到,"日志脱敏"这一设计思想在仓库中已被扩展应用到更广泛的敏感信息输出场景,而不仅仅局限于错误消息。

八、使用建议与限制

  1. 开启方式:执行SET GLOBAL tidb_redact_log = 'ON';使全部连接生效;当前仓库该变量同时支持 Global 与 Session 作用域,需要立即在当前会话生效可执行SET SESSION tidb_redact_log = 'ON';
  2. 模式选择
    • 追求绝对安全、不需要事后还原,使用ON
    • 需要保留"哪里存在敏感信息"的线索并支持事后反脱敏处理,使用MARKER(注意处理后的日志中会出现字符);
    • 排障需要完整信息时保持OFF
  3. 限制说明:由于错误库开关是进程级单例,无法为不同 session 维护不同的错误脱敏状态,全局开关变更对所有连接生效。
  4. 验证手段:开启后可通过触发一次带数据值的错误(如重复键冲突)观察错误消息与日志中是否出现?占位符,确认脱敏已生效。

总结

tidb_redact_log是 TiDB 日志脱敏的统一切入点:它通过OFF/ON/MARKER三种枚举模式,在错误消息、SQL 语句、慢日志等输出路径上统一隐藏敏感内容;底层依托 errors 库的全局单例开关与 pkg/util/redact/redact.go 中的工具函数实现;MARKER模式配合DeRedactFile工具可在事后对日志执行恢复或彻底清除。对于任何需要将日志交予外部或长期归档的场景,建议优先评估开启tidb_redact_log,从源头阻断敏感数据的扩散。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询