带着问题读 TiDB 源码:Hive 元数据使用 TiDB 启动报错

【是否原创】是
【首发渠道】TiDB 社区

《带着问题读源码系列》- 开篇

在 TiDB 社区活跃较久的伙伴们应该知道,过去我们有被称为 24 章经的《TiDB 源码阅读系列文章》,也有面向 TiKV 的《TiKV 源码解析系列文章》以及 《Deep Dive TiKV 系列文章》。这些系列文章的内容非常深入,能够帮助大家从非常细节的原理入手,了解 TiDB 以及 TiKV 的实现方式和基础原理。

然而在 TiDB 社区中活跃的许多伙伴还需要更简单,并且同自己每天工作中使用 TiDB 时遇到的问题更相关的源码阅读文章。本文是《带着问题读源码系列》的第一次尝试,在定位并解决用户所遇到的一个简单问题的过程中,对相关的代码一并进行介绍。希望能够从不同的视角,以不同的问题颗粒度,来帮助大家更好的学习 TiDB 和 TiKV 的源码。

AskTUG 上有许多用户日常使用 TiDB 过程中遇到的问题反馈,这些问题都能够成为同本文类似的源码解析素材。如果本文能够为大家创造价值,那么我们一定努力将《带着问题读源码系列》持续建设成同前辈们一样受广大家欢迎的源码阅读系列。

问题

近期在 AskTUG 论坛接到用户反馈使用 TiDB 作为 Hive metastore 数据库时设置 SERIALIZABLE 事务隔离级别失败

并且用户根据文档建议进行 SET GLOBAL tidb_skip_isolation_level_check=1 操作后,仍然无法按照预期解决问题。

考虑到知乎在一年前就已正式上线,并一直使用着 4.0.x 系列的 TiDB 作为 Hive metastore 的数据库,而用户按照说明文档操作,仍然无法顺利在 TiDB 上部署 Hive metastore,意味着很可能 TiDB 在不同的版本间发生了不兼容的行为改变。接下来就让我们一起从问题的排查入手,学习了解相应功能背后的源代码。

验证流程

在 tiup 的帮助下我们能够非常轻松的启动多个不同版本的 TiDB,对事务隔离级别的行为进行测试和验证。

首先我们先启动 5.0.0 版本的 TiDB 集群准备测试:

接下来我们使用 tiup 提示的连接命令,使用 mysql client 连接上测试集群。在设置完 SET GLOBAL tidb_skip_isolation_level_check=1 之后,使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 验证行为符合预期。说明 TiDB 5.0 系列的行为同 4.0 一致,能够支撑 Hive metastore 的运转。

image

接下来我们启动 5.1.0 版本的 TiDB 集群准备测试:

同样我们使用 mysql client 连接上测试集群,在设置完 SET GLOBAL tidb_skip_isolation_level_check=1 并重建链接确保设置生效后,使用 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 仍然会收到错误报告。说明从 TiDB 5.1 系列开始行为同以往版本不一致,无法满足 Hive metastore 的要求。

问题分析

首先我们需要 checkout 一份最新 TiDB 代码(git hash: 649ed6abc9790cfdd2a17065118379d8abcc7595),查看事务隔离级别校验相关逻辑。为了快速定位到相关逻辑所在的代码,我们可以在 TiDB 代码的根目录下对字符串 SERIALIZABLE 进行文本检索,快速定位到可能与此有关的代码文件。

我们发现实际上包含字符串 SERIALIZABLE 的文件有两个,而其中对隔离级别进行判断进行处理的文件只有 sessionctx/variable/varsutil.go 这一个文件。打开文件后我们发现这里正是对隔离级别进行判断并根据 tidb_skip_isolation_level_check 设置决定是否通过的逻辑。

我们可以同行为符合预期的 5.0 版本 TiDB 代码(git hash: 53251a9731da02ad9ee5abed9f27a14c7dea33a4)进行对比,来快速定位两者间行为不同是由那些变化引起的。同样我们通过字符串匹配快速定位到 sessionctx/variable/sysvar.go 和 sessionctx/variable/session.go 两个文件都存在对隔离级别进行条件处理的情况。

这两个不同的检查逻辑非常类似,都是试图获取 TiDBSkipIsolationLevelCheck 变量的设置,根据设定值决定是否予以放行。当我们将这里的逻辑同 master 代码中的逻辑进行对比时,我们发现他们本质上的区别非常小。

5.0 中使用了一个内置工具函数 GetSessionSystemVar 来获取变量值,而 master 代码则直接访问 SessionVars 的 systems 变量表进行访问来获取 TiDBSkipIsolationLevelCheck 变量的当前值。

进一步查看 5.0 中 GetSessionSystemVar 的实现,我们发现这个工具函数负责在 session 变量未设置时进一步到全局变量表中进行查找,并将查找到的结果放置在 SessionVars 的 systems 变量表中供后续查找使用。

根据目前的线索猜测,在 5.1 某次代码重构,试图将两个相似的重复隔离级别检查逻辑合并成一个通用逻辑的时候,绕过了工具函数直接访问 systems 变量表。这种方式访问变量表,不具备从前工具函数自动回退全局变量设定的能力。了解到这里修复非常简单,只需使用当前 TiDB 中类似工具函数 GetSessionOrGlobalSystemVar,来读取 TiDBSkipIsolationLevelCheck 的变量值就能恢复预期行为。

修复并完成构建后,再次测试 TiDB 的行为已符合预期。

提交修复

根据 TiDB 社区标准的代码贡献流程,我们首先创建一个新的 Issue,对发现的问题、复现方式以及期望的行为做清晰的描述。

创建完 issue 后,我们就可以将修复逻辑提交到自己 fork 的仓库,并创建 PR,创建过程中需要根据实际情况填充 PR 信息模版。

创建完成后 CI 系统会对提交的 PR 进行一系列的负责检查,并执行必要的测试,除了这些系统自动化的验证之外。其他社区贡献者会对 PR 进行 code review,在有足够来自于 TiDB Reviewer 及以上权限的贡献者对 PR 点赞后,变更才能够被合并到项目主干中。

在 PR 提交后不久就得到了 @morgo 的 review 反馈,反馈一针见血地指出了问题背后的真正原因是 PR #24836 中对 TiDBSkipIsolationLevelCheck 变量初始化行为的错误变更。去掉 TiDBSkipIsolationLevelCheck 变量定义中的 skipInit: true 初始化字段,即可确保 session 初始化时,正确地将 global 变量值复制到 session 中,让前面的隔离级别检查逻辑行为恢复正常。

根据这个线索进行代码修改,并实际测试证明表现符合预期,接下来让我们继续分析 skipInit 相关的源码探个究竟。

代码中所有对 skipInit 变量的读取操作都封装在上图的 SkipInit 函数中,从下图中我们可以看到 SkipInit 方法用于在初始化新的 session 变量 cache 的过程跳过部分变量。

接下来 newSessionCache 被更新到 session 变量中,并通过下图中的 GetSessionCache 方法对外提供访问。

而 GetSessionCache 方法只有一个调用方 loadCommonGlobalVariablesIfNeeded,到这里 skipInit 对系统变量初始化流程的影响就非常清晰了。

当 session 创建完成后,没有标记为 skipInit 的变量,都会以变量的初始值的形式更新到会话变量表中,也就是前面提到的 systems 变量表中。当我们将 TiDBSkipIsolationLevelCheck 的 skipInit 恢复为 false 之后,全局变量 tidb_skip_isolation_level_check 能够在这个初始化的过程中被正确的复制到用户会话,使得调整会话事务隔离级别的行为符合用户预期。

在问题得到解决后,大家可能还会问在什么样的情况下 skipInit 需要被设置成 true。在引入这个功能的 PR #24836 中我们可以得知部分不适合在初始化过程中,复制到会话中的变量会利用这个标记实现黑名单功能。而在这次重构过程中 TiDBSkipIsolationLevelCheck 被错误的设置在黑名单中,导致了 5.1 开始版本行为的异常。

What problem does this PR solve?

Problem Summary:

Currently the builtinGlobalVariable feature is a source of bugs because even though a sysvar is added, it is not automatically copied to new sessions. This behavior is also not MySQL compatible, where it is expected a sysvar of session scope should be copied on session init.

Fixing the full incompatibility is a little bit more complicated, but this takes the initial step of inverting from an allow list to a deny list, but is otherwise functionally compatible.

This also includes the fix from #24835 should this PR supercede it.

What is changed and how it works?

What’s Changed:

A variable on the SysVar struct can now be set to skipInit . By default it will not skip for session-scope variables, which is why it is now a deny list.

However, it will always skip for noop variables, which helps keep the memory footprint of new sessions slightly lower.

Related changes

  • None
  • There will need to be followup PRs to handle the specific skipInit variables; some probably don’t need to be on this list. The global-only variables that are hard-coded into the SkipInit() function will also need removing.

后记

感谢向社区报告 TiDB 行为异常的热心用户 @向阳煦啊啊,非常遗憾没能在故障发生的第一时间,定位并解决问题。但我们仍然希望在新版本发布修复这个问题后,TiDB 能够为你支撑 Hive metastore,乃至更多业务场景起到积极作用。

6赞

呱唧呱唧:clap::clap:

实名打 call~

很有启发意义,必须 :clap::clap::clap:

谢谢!后续我们团队也会结合业务场景使用TiDB的:grin:

1赞

感谢你给我们提供有可复现的 BUG ,
已为你完成100经验、100积分的奖励~
我代表 TiDB 社区管理人员对你表示感谢

这个可以有~ 回头我们也试着跟这个系列:expressionless:

1赞