背景

上一篇《记一次简单的Oracle离线数据迁移至TiDB过程》说到在使用Lightning导入csv文件到TiDB的时候发现了一个bug,是这样一个过程。

Oracle源库中表名都是大写,经过前文所述的方法导入到TiDB后表名也是保持全大写,数据同步过程非常顺利。

第二天我把整套操作流程教给一位新手朋友,他就挑了一张表用来做实验,结果死活都不行。各种分析和重试都没有效果,就在快要懵逼的时候想到了这个大小写问题,把csv拉出来一看是个全小写的文件名,我尝试着把表名改成大写再导入一次,这次终于成功了。

原来,是这位小伙子用sqluldr2导出表数据的时候把文件名写死了,而且是个小写。。。

这里提一下TiDB表名大小写敏感相关的参数lower-case-table-names,这个参数只能被设置成2,也就是存储表名的时候区分大小写,对比的时候统一转为小写。因此,TiDB中的表名建议使用全小写来命名。

这个特性基本和MySQL是一致的,只是MySQL支持更多的场景,具体可以参考https://dev.mysql.com/doc/refman/5.7/en/identifier-case-sensitivity.html

那么,说好的TiDB表名不区分大小写呢,怎么用了Lightning就失效了?

Bug重现

上面说的还是有点抽象,我们通过如下的步骤重现一下。

这里我准备的TiDB测试版本是v5.2.2,和前面发现bug的版本一致,Lightning也使用配套的版本。我拿最新的master分支也能复现这个问题。

先创建一张测试表,表名全部用大写:

  1. use test;
  2. create table LIGHTNING_BUG (f1 varchar(50),f2 varchar(50),f3 varchar(50));

再准备一个待导入的csv文件,文件名是test.lightning_bug.csv

  1. 111|aaa|%%%
  2. 222|bbb|###

Lightning的完整配置文件:

  1. [lightning]
  2. level = "info"
  3. file = "tidb-lightning.log"
  4. index-concurrency = 2
  5. table-concurrency = 5
  6. io-concurrency = 5
  7. [tikv-importer]
  8. backend = "local"
  9. sorted-kv-dir = "/tmp/tidb/lightning_dir"
  10. [mydumper]
  11. data-source-dir = "/tmp/tidb/data"
  12. no-schema = true
  13. filter = ['*.*']
  14. [mydumper.csv]
  15. # 字段分隔符,支持一个或多个字符,默认值为 ','。
  16. separator = '|'
  17. # 引用定界符,设置为空表示字符串未加引号。
  18. delimiter = ''
  19. # 行尾定界字符,支持一个或多个字符。设置为空(默认值)表示 "\n"(换行)和 "\r\n" (回车+换行),均表示行尾。
  20. terminator = ""
  21. # CSV 文件是否包含表头。
  22. # 如果 header = true,将跳过首行。
  23. header = false
  24. # CSV 文件是否包含 NULL。
  25. # 如果 not-null = true,CSV 所有列都不能解析为 NULL。
  26. not-null = false
  27. # 如果 not-null = false(即 CSV 可以包含 NULL),
  28. # 为以下值的字段将会被解析为 NULL。
  29. null = '\N'
  30. # 是否对字段内“\“进行转义
  31. backslash-escape = true
  32. # 如果有行以分隔符结尾,删除尾部分隔符。
  33. trim-last-separator = false
  34. [tidb]
  35. host = "x.x.x.x"
  36. port = 4000
  37. user = "root"
  38. password = ""
  39. status-port = 10080
  40. pd-addr = "x.x.x.x:2379"
  41. [checkpoint]
  42. enable = false
  43. [post-restore]
  44. checksum = false
  45. analyze = false

运行如下命令开始执行导入任务:

  1. ./tidb-lightning --config tidb-lightning.toml --check-requirements=false

报错信息:

日志里面全部是Info,除了没有正常输出tidb lightning exit以外,看不到任何报错,一幅岁月静好的样子:

我认为这里的主要问题是,panic非常不友好,而且提示信息不够明确,虽然说了是空指针异常不过没什么参考价值,当时还被segmentation violation误导了好久,一直怀疑是数据格式有问题。

我意识到这个bug应该不难,于是自己拉了一份TiDB源码开始定位问题。

Lightning的处理流程

Lightning的入口文件是br/cmd/tidb-lightning/main.go,而它的核心实现都放在br/pkg/lightning目录下。

我根据报错的堆栈信息倒推整个Lightning的导入流程,首先定位到restore.go文件第1311行,我看到如下代码:

根据直觉,猜测tableInfo是一个nil值,以至于在取tableInfo.Name的时候报出空指针异常。如果是这样的话,证明是表名不存在导致,但我记得表不存在的时候它的报错信息是这样:

所以说在此之前的某个地方,它一定是把大写表名和小写表名匹配上的,我们继续往上翻。

在报错的这个地方,需要重点关注两个被对比的map对象rc.dbMetasrc.dbInfos,报错的原因是dbMetas里的表在dbInfos里面找不到,那我们就分别看看这两个对象是干嘛用的。

通过查找这行代码所在的方法restoreTables调用关系,发现了Lightning的主要导入流程:

  1. func (rc *Controller) Run(ctx context.Context) error {
  2. opts := []func(context.Context) error{
  3. rc.setGlobalVariables,
  4. rc.restoreSchema,
  5. rc.preCheckRequirements,
  6. rc.restoreTables,
  7. rc.fullCompact,
  8. rc.switchToNormalMode,
  9. rc.cleanCheckpoints,
  10. }
  11. ....
  12. for i, process := range opts {
  13. err = process(ctx)
  14. ....
  15. }
  16. ....
  17. }

这里的主要流程就是restoreSchemarestoreTables,我们一会再来细看,先继续往上翻。

再上一层是lightning.go文件的run方法,在这儿我们找到了那个dbMetas是怎么来的:

  1. func (l *Lightning) run(taskCtx context.Context, taskCfg *config.Config, g glue.Glue) (err error) {
  2. ...
  3. dbMetas := mdl.GetDatabases()
  4. web.BroadcastInitProgress(dbMetas)
  5. var procedure *restore.Controller
  6. procedure, err = restore.NewRestoreController(ctx, dbMetas, taskCfg, s, g)
  7. if err != nil {
  8. log.L().Error("restore failed", log.ShortError(err))
  9. return errors.Trace(err)
  10. }
  11. defer procedure.Close()
  12. err = procedure.Run(ctx)
  13. return errors.Trace(err)
  14. }

通过一路追踪进去,发现dbMetas就是通过解析要导入的文件名来获得数据库名称和表名称的,也就是说它存放着要被导入的Schema信息,这也是为什么csv文件要按照{dbname}.{tablename}.csv来命名的原因

Tips:其实这个格式是可以通过[mydumper.files]自定义的,上面这种是默认格式。

再往上的话就是RunOnce方法,这是main函数的调用入口,它传入了一个空的上下文对象,以及配置文件信息:

  1. /// br > pkg > lightning > lightning.go
  2. func (l *Lightning) RunOnce(taskCtx context.Context, taskCfg *config.Config, glue glue.Glue) error {
  3. if err := taskCfg.Adjust(taskCtx); err != nil {
  4. return err
  5. }
  6. taskCfg.TaskID = time.Now().UnixNano()
  7. ...
  8. return l.run(taskCtx, taskCfg, glue)
  9. }
  10. /// br > cmd > tidb-lightning > main.go
  11. func main() {
  12. globalCfg := config.Must(config.LoadGlobalConfig(os.Args[1:], nil))
  13. ....
  14. err = func() error {
  15. if globalCfg.App.ServerMode {
  16. return app.RunServer()
  17. }
  18. cfg := config.NewConfig()
  19. if err := cfg.LoadFromGlobal(globalCfg); err != nil {
  20. return err
  21. }
  22. return app.RunOnce(context.Background(), cfg, nil)
  23. }()
  24. ....
  25. }

整个过程还是比较清晰的,核心处理逻辑都放在Restore Controller里面。

按照前面的分析,似乎只要在报错的地方判断一下 nil就行了,但判断之后我该做如何处理呢?感觉只是治标不治本,还需要进一步分析下。

对Bug的思考

深度分析之前再看一个现象,我把最开始的导入命令去掉--check-requirements=false参数,看到如下提示:

貌似lightning本身是能识别到大小写的差异呀(看到这里我一度认为修复方法是提示表不存在),再结合之前提到的table schema not found报错,我觉得事情有点诡异。

深扒源码发现,Lightning是能够对上下游Schema做非常细致的检查,这部分逻辑被封装在SchemaIsValid方法中,只有在--check-requirements=true的时候才会启用,这里的检查包括库表名称、字段数量、数据文件、csv表头等等。那table schema not found又是怎么回事?

前面提到dbMetas是通过解析文件名获取,我们再看看dbInfos是如何获取的。回到之前提到的restoreSchema方法,我看到如下代码:

  1. getTableFunc := rc.backend.FetchRemoteTableModels
  2. ....
  3. err := worker.makeJobs(rc.dbMetas, getTableFunc)
  4. ....
  5. dbInfos, err := LoadSchemaInfo(ctx, rc.dbMetas, getTableFunc)
  6. if err != nil {
  7. return errors.Trace(err)
  8. }
  9. rc.dbInfos = dbInfos
  10. ....

从这里可以看到,获取目标库的表清单是通过各自Backend提供的远程方式读取的,对于local模式而言,实际就是调用TiDB的状态端口去获取(现在知道配置文件中10080的作用了吧):

  1. curl http://{tidb-server}:10080/schema/test

makeJobs方法是创建Schema的核心实现,主要包括恢复数据库、恢复表结构、恢复视图3部分。看如下一部分代码;

  1. // 2. restore tables, execute statements concurrency
  2. for _, dbMeta := range dbMetas {
  3. // we can ignore error here, and let check failed later if schema not match
  4. tables, _ := getTables(worker.ctx, dbMeta.Name)
  5. tableMap := make(map[string]struct{})
  6. for _, t := range tables {
  7. tableMap[t.Name.L] = struct{}{}
  8. }
  9. for _, tblMeta := range dbMeta.Tables {
  10. if _, ok := tableMap[strings.ToLower(tblMeta.Name)]; ok {
  11. // we already has this table in TiDB.
  12. // we should skip ddl job and let SchemaValid check.
  13. continue
  14. } else if tblMeta.SchemaFile.FileMeta.Path == "" {
  15. return errors.Errorf("table `%s`.`%s` schema not found", dbMeta.Name, tblMeta.Name)
  16. }
  17. ...
  18. }
  19. ...

这里很让人迷惑,它检查表是否存在的时候是用全小写去判断的,和前面的SchemaIsValid方法不一致,我又认为修复方法应该是转为全小写判断了。。。

我们再来看LoadSchemaInfo方法,从代码来看它就是产生dbInfos的地方,而这个对象存放的是目标库的实际Schema信息,下面这段代码是重头戏:

  1. func LoadSchemaInfo(
  2. ctx context.Context,
  3. schemas []*mydump.MDDatabaseMeta,
  4. getTables func(context.Context, string) ([]*model.TableInfo, error),
  5. ) (map[string]*checkpoints.TidbDBInfo, error) {
  6. result := make(map[string]*checkpoints.TidbDBInfo, len(schemas))
  7. for _, schema := range schemas {
  8. tables, err := getTables(ctx, schema.Name)
  9. if err != nil {
  10. return nil, err
  11. }
  12. tableMap := make(map[string]*model.TableInfo, len(tables))
  13. for _, tbl := range tables {
  14. tableMap[tbl.Name.L] = tbl
  15. }
  16. dbInfo := &checkpoints.TidbDBInfo{
  17. Name: schema.Name,
  18. Tables: make(map[string]*checkpoints.TidbTableInfo),
  19. }
  20. for _, tbl := range schema.Tables {
  21. tblInfo, ok := tableMap[strings.ToLower(tbl.Name)]
  22. if !ok {
  23. return nil, errors.Errorf("table '%s' schema not found", tbl.Name)
  24. }
  25. tableName := tblInfo.Name.String()
  26. if tblInfo.State != model.StatePublic {
  27. err := errors.Errorf("table [%s.%s] state is not public", schema.Name, tableName)
  28. metric.RecordTableCount(metric.TableStatePending, err)
  29. return nil, err
  30. }
  31. metric.RecordTableCount(metric.TableStatePending, err)
  32. if err != nil {
  33. return nil, errors.Trace(err)
  34. }
  35. tableInfo := &checkpoints.TidbTableInfo{
  36. ID: tblInfo.ID,
  37. DB: schema.Name,
  38. Name: tableName,
  39. Core: tblInfo,
  40. }
  41. dbInfo.Tables[tableName] = tableInfo
  42. }
  43. result[schema.Name] = dbInfo
  44. }
  45. return result, nil
  46. }

看到这里好像真相大白了,前半部分都一直用小写匹配,到取tableName的时候貌似忘了这个事???

最后看看tblInfo.Name.String()返回的是啥:

  1. // CIStr is case insensitive string.
  2. type CIStr struct {
  3. O string `json:"O"` // Original string.
  4. L string `json:"L"` // Lower case string.
  5. }
  6. // String implements fmt.Stringer interface.
  7. func (cis CIStr) String() string {
  8. return cis.O
  9. }

这样来看,SchemaIsValid其实是受到了LoadSchemaInfo的影响,给人一种能够区分大小写的假象。

我的修复思路

上面的分析过程也提到了我的修复思路的变化,汇总有以下两种办法:

第一种,在报错的地方做nil值判断提示表结构不存在,但是碰到这个提示后是继续导入还是整个任务退出需要深度考虑一下,如果还有类似的问题是不是也这样去修复。

第二种,整个逻辑全部转为全小写去判断,从根源上解决问题,这样的话我觉得有两个好处,一个是避免大小写引发新的bug,二是TiDB的表名本身就是不区分大小写。

接下来,我会按第二种方式提交PR尝试修复这个问题。

不过,针对这个bug我又想起了另一种情况,就是数据库表名是小写文件名是大写,我测试了会有相同的问题。

总结

在TiDB中给Schema对象命名的时候养成好习惯,统一使用小写,避免引起不必要的麻烦。

在使用Lightning的时候,不要轻易关闭check-requirements,它会帮你提前预判很多风险,这点还是很重要的。

从一些TiDB工具的使用经验上来看,它们的很多异常提示并不是很友好,这样会让用户多走弯路,希望官方能关注下这块的优化。

还有就是,碰到报错不要慌(实际上在客户现场的时候慌的一批),啃一啃源码也挺有意思的。

探索TiDB Lightning的源码来解决发现的bug的更多相关文章

  1. Android Studio2.x版本无法自动关联源码的解决方法

    Android Studio2.x版本无法自动关联源码的解决方法 在学习android开发过程中,对于一个不熟悉的类,阅读源码是一个很好的学习方式,使用andorid studio开发工具的SDK M ...

  2. 《深入探索Netty原理及源码分析》文集小结

    <深入探索Netty原理及源码分析>文集小结 https://www.jianshu.com/p/239a196152de

  3. 清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)C#版

    目录 说明 TCP与UDP通信的特点 TCP中的沾包现象 自定义应用层协议 TCPLibrary通信库介绍 Demo演示 未完成功能 源码下载 说明 我前面博客中有多篇文章讲到了.NET中的网络编程, ...

  4. jQuery源码研究——解决命名冲突

    在项目中难免不去使用多个插件,如此一来这些插件就有可能出现一样的名称,当出现同名变量时后一个将会覆盖上一个,这样的话我们就无法同时使用多个插件了. 当遇到这种情况我们可以手动去修改插件源码把它的名字改 ...

  5. 使用myeclipse自动导入hibernate3的jar包,如何关联hibernate源码的解决办法

    1.在网上找了好久,今天终于解决了,如果你的myeclipse自动生成的添加hibernate3jar包时,依靠通常的方法是无法关联其相应版本的源代码的,就是你在编写代码是,按住ctrl + hibe ...

  6. 被Spring坑了一把,查看源码终于解决了DataFlow部署K8s应用的问题

    1 前言 欢迎访问南瓜慢说 www.pkslow.com获取更多精彩文章! Docker & Kubernetes相关文章:容器技术 基于各种原因,团队的Kubernetes被加了限制,必须在 ...

  7. 【源码】Redis exists命令bug分析

    本文基于社区版Redis 4.0.8 1.复现条件 版本:社区版Redis 4.0.10以下版本 使用场景:开启读写分离的主从架构或者集群架构(master只负责写流量,slave负责读流量) 案例: ...

  8. Java源码中的发现:快速判断一个int值是几位数

    判断一个int值是几位数,要是我自己实现,估计又会想到除法和模运算了,偶然在java标准API源码中发现的写法,很强大. public class Test { final static int[] ...

  9. JGUI源码:解决手机端点击出现半透明阴影(4)

    下面开始进入正题,问题发现与解决 1.According解决手机浏览器点击出现半透明阴影 手机下点击有白色蒙版,原始效果如下,看起来很不协调 2.解决办法:增加 -webkit-tap-highlig ...

随机推荐

  1. Redis集群安装详细步骤

    环境: Centos7    redis3.0 三台虚拟机主机名分别为 master   node1  node2 如果单机的时候设置过密码最好把密码去掉,避免位置的错误. 拍个快照方便恢复. 1.创 ...

  2. Func<>用法

    Func是一个委托,委托里面可以存方法,Func<string,string>或Func<string,string,int,string>等 前几个是输入参数,最后一个是返回 ...

  3. Window 共享内存

    转载请注明来源:https://www.cnblogs.com/hookjc/ C++使用共享内存实现进程间通信文件映射是一种实现进程间单向或双向通信的机制.它允许两个或多个本地进程间相互通信.为了共 ...

  4. linux下打包所有文件,包括隐藏文件到压缩包

    命令如下: cd /root/test/ tar czvf test.tar.gz .[!.]* * 解释: tar czvf test.tar.gz * 压缩当前文件夹下非[隐藏文件]的文件 tar ...

  5. #pragma mark指令

    1.#pragma mark指令的使用 功能:简单来说就是对代码的分组,方便代码查找和导航用的 它们告诉Xcode编译器,要在编辑器窗格顶部的方法和函数弹出菜单中将代码分隔开.一些类(尤其是一些控制器 ...

  6. tabbar选中按钮的标题颜色和字体

    @implementation XMGTabBarController /* 问题: 1.选中按钮的图片被渲染 -> iOS7之后默认tabBar上按钮图片都会被渲染 1.修改图片 2.通过代码 ...

  7. 帆软报表(finereport)鼠标悬停背景变色

    在报表中,为了突出鼠标所在单元格,当鼠标悬浮时突出背景色(字体),鼠标离开后恢复原有的背景色(字体). 在设计器 模板>模板 Web 属性>填报页面设置,去除填报当前编辑行背景设置的勾选, ...

  8. Solution -「牛客 NOIP 模拟赛」打拳

    \(\mathcal{Description}\)   现 \(2^n\) 个人进行淘汰赛,他们的战力为 \(1\sim 2^n\),战力强者能战胜战力弱者,但是战力在集合 \(\{a_m\}\) 里 ...

  9. figlet 一个在linux生成字符串图案的玩具

    figlet官网 figlet官方字体库 figlet字体样例 安装方法 centos/redhat/fedora 发行版 yum install -y figlet debian/ubuntu 发行 ...

  10. suse 12 二进制部署 Kubernetets 1.19.7 - 第12章 - 部署dashboard插件

    文章目录 1.12.0.创建namespace 1.12.1.创建Dashboard rbac文件 1.12.2.创建dashboard文件 1.12.3.查看pod以及svc 1.12.4.获取 d ...