背景
上一篇《记一次简略的 Oracle 离线数据迁徙至 TiDB 过程》说到在应用 Lightning 导入 csv 文件到 TiDB 的时候发现了一个 bug,是这样一个过程。
Oracle 源库中表名都是大写,通过前文所述的办法导入到 TiDB 后表名也是放弃全大写,数据同步过程十分顺利。
第二天我把整套操作流程教给一位老手敌人,他就挑了一张表用来做试验,后果死活都不行。各种剖析和重试都没有成果,就在快要懵逼的时候想到了这个大小写问题,把 csv 拉进去一看是个全小写的文件名,我尝试着把表名改成大写再导入一次,这次终于胜利了。
原来,是这位小伙子用 sqluldr2 导出表数据的时候把文件名写死了,而且是个小写。。。
这里提一下 TiDB 表名大小写敏感相干的参数
lower-case-table-names
,这个参数只能被设置成 2,也就是存储表名的时候辨别大小写,比照的时候对立转为小写。因而,TiDB 中的表名倡议应用全小写来命名。这个个性根本和 MySQL 是统一的,只是 MySQL 反对更多的场景,具体能够参考 https://dev.mysql.com/doc/ref…
那么,说好的 TiDB 表名不辨别大小写呢,怎么用了 Lightning 就生效了?
Bug 重现
下面说的还是有点形象,咱们通过如下的步骤重现一下。
这里我筹备的 TiDB 测试版本是 v5.2.2,和后面发现 bug 的版本统一,Lightning 也应用配套的版本。我拿最新的 master 分支也能复现这个问题。
先创立一张测试表,表名全副用大写:
use test;
create table LIGHTNING_BUG (f1 varchar(50),f2 varchar(50),f3 varchar(50));
再筹备一个待导入的 csv 文件,文件名是test.lightning_bug.csv
:
111|aaa|%%%
222|bbb|###
Lightning 的残缺配置文件:
[lightning]
level = "info"
file = "tidb-lightning.log"
index-concurrency = 2
table-concurrency = 5
io-concurrency = 5
[tikv-importer]
backend = "local"
sorted-kv-dir = "/tmp/tidb/lightning_dir"
[mydumper]
data-source-dir = "/tmp/tidb/data"
no-schema = true
filter = ['*.*']
[mydumper.csv]
# 字段分隔符,反对一个或多个字符,默认值为 ','。separator = '|'
# 援用定界符,设置为空示意字符串未加引号。delimiter = ''# 行尾定界字符,反对一个或多个字符。设置为空(默认值)示意"\n"(换行)和"\r\n"(回车 + 换行),均示意行尾。terminator = ""
# CSV 文件是否蕴含表头。# 如果 header = true,将跳过首行。header = false
# CSV 文件是否蕴含 NULL。# 如果 not-null = true,CSV 所有列都不能解析为 NULL。not-null = false
# 如果 not-null = false(即 CSV 能够蕴含 NULL),# 为以下值的字段将会被解析为 NULL。null = '\N'
# 是否对字段内“\“进行本义
backslash-escape = true
# 如果有行以分隔符结尾,删除尾部分隔符。trim-last-separator = false
[tidb]
host = "x.x.x.x"
port = 4000
user = "root"
password = ""
status-port = 10080
pd-addr = "x.x.x.x:2379"
[checkpoint]
enable = false
[post-restore]
checksum = false
analyze = false
运行如下命令开始执行导入工作:
./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.dbMetas
和rc.dbInfos
,报错的起因是 dbMetas
里的表在 dbInfos
外面找不到,那咱们就别离看看这两个对象是干嘛用的。
通过查找这行代码所在的办法 restoreTables
调用关系,发现了 Lightning 的次要导入流程:
func (rc *Controller) Run(ctx context.Context) error {opts := []func(context.Context) error{
rc.setGlobalVariables,
rc.restoreSchema,
rc.preCheckRequirements,
rc.restoreTables,
rc.fullCompact,
rc.switchToNormalMode,
rc.cleanCheckpoints,
}
....
for i, process := range opts {err = process(ctx)
....
}
....
}
这里的次要流程就是 restoreSchema
和restoreTables
,咱们一会再来细看,先持续往上翻。
再上一层是 lightning.go
文件的 run
办法,在这儿咱们找到了那个 dbMetas
是怎么来的:
func (l *Lightning) run(taskCtx context.Context, taskCfg *config.Config, g glue.Glue) (err error) {
...
dbMetas := mdl.GetDatabases()
web.BroadcastInitProgress(dbMetas)
var procedure *restore.Controller
procedure, err = restore.NewRestoreController(ctx, dbMetas, taskCfg, s, g)
if err != nil {log.L().Error("restore failed", log.ShortError(err))
return errors.Trace(err)
}
defer procedure.Close()
err = procedure.Run(ctx)
return errors.Trace(err)
}
通过一路追踪进去,发现 dbMetas
就是通过解析要导入的文件名来取得数据库名称和表名称的,也就是说它寄存着要被导入的 Schema 信息,这也是为什么 csv 文件要依照 {dbname}.{tablename}.csv
来命名的起因。
Tips:其实这个格局是能够通过 [mydumper.files] 自定义的,下面这种是默认格局。
再往上的话就是 RunOnce
办法,这是 main
函数的调用入口,它传入了一个空的上下文对象,以及配置文件信息:
/// br > pkg > lightning > lightning.go
func (l *Lightning) RunOnce(taskCtx context.Context, taskCfg *config.Config, glue glue.Glue) error {if err := taskCfg.Adjust(taskCtx); err != nil {return err}
taskCfg.TaskID = time.Now().UnixNano()
...
return l.run(taskCtx, taskCfg, glue)
}
/// br > cmd > tidb-lightning > main.go
func main() {globalCfg := config.Must(config.LoadGlobalConfig(os.Args[1:], nil))
....
err = func() error {
if globalCfg.App.ServerMode {return app.RunServer()
}
cfg := config.NewConfig()
if err := cfg.LoadFromGlobal(globalCfg); err != nil {return err}
return app.RunOnce(context.Background(), cfg, nil)
}()
....
}
整个过程还是比拟清晰的,外围解决逻辑都放在 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
办法,我看到如下代码:
getTableFunc := rc.backend.FetchRemoteTableModels
....
err := worker.makeJobs(rc.dbMetas, getTableFunc)
....
dbInfos, err := LoadSchemaInfo(ctx, rc.dbMetas, getTableFunc)
if err != nil {return errors.Trace(err)
}
rc.dbInfos = dbInfos
....
从这里能够看到,获取指标库的表清单是通过各自 Backend 提供的近程形式读取的,对于 local 模式而言,理论就是调用 TiDB 的状态端口去获取(当初晓得配置文件中 10080 的作用了吧):
curl http://{tidb-server}:10080/schema/test
makeJobs
办法是创立 Schema 的外围实现,次要包含复原数据库、复原表构造、复原视图 3 局部。看如下一部分代码;
// 2. restore tables, execute statements concurrency for _, dbMeta := range dbMetas {// we can ignore error here, and let check failed later if schema not match tables, _ := getTables(worker.ctx, dbMeta.Name) tableMap := make(map[string]struct{}) for _, t := range tables {tableMap[t.Name.L] = struct{}{} } for _, tblMeta := range dbMeta.Tables {if _, ok := tableMap[strings.ToLower(tblMeta.Name)]; ok {// we already has this table in TiDB. // we should skip ddl job and let SchemaValid check. continue} else if tblMeta.SchemaFile.FileMeta.Path == ""{return errors.Errorf("table `%s`.`%s` schema not found", dbMeta.Name, tblMeta.Name) } ... } ...
这里很让人蛊惑,它检查表是否存在的时候是用全小写去判断的,和后面的 SchemaIsValid
办法不统一,我又认为修复办法应该是转为全小写判断了。。。
咱们再来看 LoadSchemaInfo
办法,从代码来看它就是产生 dbInfos
的中央,而这个对象寄存的是指标库的理论 Schema 信息,上面这段代码是重头戏:
func LoadSchemaInfo(ctx context.Context, schemas []*mydump.MDDatabaseMeta, getTables func(context.Context, string) ([]*model.TableInfo, error),) (map[string]*checkpoints.TidbDBInfo, error) {result := make(map[string]*checkpoints.TidbDBInfo, len(schemas)) for _, schema := range schemas {tables, err := getTables(ctx, schema.Name) if err != nil {return nil, err} tableMap := make(map[string]*model.TableInfo, len(tables)) for _, tbl := range tables {tableMap[tbl.Name.L] = tbl } dbInfo := &checkpoints.TidbDBInfo{Name: schema.Name, Tables: make(map[string]*checkpoints.TidbTableInfo), } for _, tbl := range schema.Tables {tblInfo, ok := tableMap[strings.ToLower(tbl.Name)] if !ok {return nil, errors.Errorf("table'%s'schema not found", tbl.Name) } tableName := tblInfo.Name.String() if tblInfo.State != model.StatePublic { err := errors.Errorf("table [%s.%s] state is not public", schema.Name, tableName) metric.RecordTableCount(metric.TableStatePending, err) return nil, err } metric.RecordTableCount(metric.TableStatePending, err) if err != nil {return nil, errors.Trace(err) } tableInfo := &checkpoints.TidbTableInfo{ID: tblInfo.ID, DB: schema.Name, Name: tableName, Core: tblInfo,} dbInfo.Tables[tableName] = tableInfo } result[schema.Name] = dbInfo } return result, nil}
看到这里如同水落石出了,前半部分都始终用小写匹配,到取 tableName
的时候貌似忘了这个事???
最初看看 tblInfo.Name.String()
返回的是啥:
// CIStr is case insensitive string.type CIStr struct {O string `json:"O"` // Original string. L string `json:"L"` // Lower case string.}// String implements fmt.Stringer interface.func (cis CIStr) String() string { return cis.O}
这样来看,SchemaIsValid
其实是受到了 LoadSchemaInfo
的影响,给人一种可能辨别大小写的假象。
我的修复思路
下面的剖析过程也提到了我的修复思路的变动,汇总有以下两种方法:
第一种,在报错的中央做 nil
值判断提醒表构造不存在,然而碰到这个提醒后是持续导入还是整个工作退出须要深度考虑一下,如果还有相似的问题是不是也这样去修复。
第二种,整个逻辑全副转为全小写去判断,从本源上解决问题,这样的话我感觉有两个益处,一个是防止大小写引发新的 bug,二是 TiDB 的表名自身就是不辨别大小写。
接下来,我会按第二种形式提交 PR 尝试修复这个问题。
不过,针对这个 bug 我又想起了另一种状况,就是数据库表名是小写文件名是大写,我测试了会有雷同的问题。
总结
在 TiDB 中给 Schema 对象命名的时候养成好习惯,对立应用小写,防止引起不必要的麻烦。
在应用 Lightning 的时候,不要轻易敞开check-requirements
,它会帮你提前预判很多危险,这点还是很重要的。
从一些 TiDB 工具的应用教训上来看,它们的很多异样提醒并不是很敌对,这样会让用户多走弯路,心愿官网能关注下这块的优化。
还有就是,碰到报错不要慌(实际上在客户现场的时候慌的一批🤣),啃一啃源码也挺有意思的。