背景
上一篇《记一次简略的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 = 2table-concurrency = 5io-concurrency = 5[tikv-importer]backend = "local"sorted-kv-dir = "/tmp/tidb/lightning_dir"[mydumper]data-source-dir = "/tmp/tidb/data"no-schema = truefilter = ['*.*'][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 = 4000user = "root"password = ""status-port = 10080pd-addr = "x.x.x.x:2379"[checkpoint]enable = false[post-restore]checksum = falseanalyze = 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.gofunc (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.gofunc 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工具的应用教训上来看,它们的很多异样提醒并不是很敌对,这样会让用户多走弯路,心愿官网能关注下这块的优化。
还有就是,碰到报错不要慌(实际上在客户现场的时候慌的一批),啃一啃源码也挺有意思的。