【是否原创】是
【首发渠道】知乎-神州数码云基地
【首发渠道链接】https://zhuanlan.zhihu.com/p/428374317
【正文】
从抓包发现并解决 Navicat 编辑 TiDB 视图报错的问题
一、引言
TiDB 是一个高度兼容MySQL协议的分布式数据库。无论是连接协议还是语法语法上,TiDB都在不断完善对于MySQL的兼容性。但对于MySQL周边工具的兼容上,TiDB 并非尽善尽美。今天要说的就是知名的数据库连接工具 Navicat 或者是 Navicat Premium 连接到 TIDB 使用其编辑视图功能时遇到的问题。
二、问题发现
Navicat 提供可视化的方法给我们来操作连接到的数据库。这是一个非常方便的功能,我们不需要牢记创建表,视图,索引等具体的SQL语法。使用 Navicat 右键选项即可完成这些操作。但当笔者使用其连接到 TIDB,并准备编辑试图时,却提示某个函数不存在的报错信息。具体是 “ERROR 1305 - FUNCTION load_file does not exist”。其实这个报错把问题说的很明白。也就是 Navicat 编辑视图时会调用load_file 这个函数。而 TIDB 目前还没有实现这个函数,从而导致编辑试图报错。
从官网文档(https://docs.pingcap.com/zh/tidb/stable)以及 asktug(https://asktug.com/) 搜索相关内容,得到两条线索:
1、TiDB 目前确实没有实现load_file函数(https://docs.pingcap.com/zh/tidb/stable/string-functions#不支持的函数)
2、Navicat 连接 TiDB 编辑试图会报错的问题早已有之,在 asktug 上有人反馈相同问题。(Tidb 3.0.2通过navicat 查看view报load_file函数不存在?)
通过以上的内容确定 Navicat 报这个错的原因在于 TiDB 目前还没有实现 load_file 函数。通过查阅MySQL官方文档可以得知 load_file 的定义:"Reads the file and returns the file contents as a string. "(https://dev.mysql.com/doc/refman/8.0/en/string-functions.html#function_load-file)。也就是说传入文件地址,返回文件的字符串形式的内容即可。实现这个函数的逻辑非常 easy。正当笔者准备打开TIDB 源码大改特改时,笔者不禁想问,Navicat 编辑视图需要 load_file 方法是为什么?基于笔者一年多以来在 TIDB 和 MySQL使用差异上踩坑的经验来看,不能盲目实现函数,要考虑到 TiDB 作为分布式数据库,其文件组织方式已与单机 MySQL 有天差地别。所以现在最好的方式就是通过抓包的方式看看 Navicat 使用 load_file 函数干了什么事。
话不多说,立马开干。一开始笔者使用最新 master 源码在本地编译运行。抓包时发现数据都是 tls 加密的。没办法看到具体内容。想了想办法,通过切到 4.0 版本的某个分支,再次本地编译启动,抓到的数据包就是非加密的。使用 show 命令查看当前版本是否默认开启 ssl 加密。如果 have_openssl,have_ssl 都是 “DISABLED”,那么说明已经关闭 ssl 加密,可以抓到报文的明文内容。
SHOW VARIABLES LIKE '%ssl%';
在报文中终于发现 Navicat 使用 load_file 的操作。它读取由系统变量 “datadir” 和一系列字符串组成的文件地址映射到 source 列。
SELECT CONVERT(load_file(concat(@@datadir, 'test', '/', 'v1', '.frm')) USING utf8) AS source
这里的 .frm 后缀文件是MySQL的表结构定义文件。从这里也可以大概了解到 Navicat 使用 load_file 的原因了。可能就是在 edit view 时需要读取相关表定义。另外一个问题。系统变量 datadir 在 TIDB 和 MySQL 中代表不同含义。在 TiDB 中查询 datadir,返回的是 pd 的2379 端口地址(比如:127.0.0.1:2379)。而 MySQL 中则是真正存储数据的目录地址。
三、问题解决
从第二点的内容来看,由于 TiDB 和 MySQL 文件组织形式的巨大差别。load_file 的结果肯定没办法满足预期。按照 MySQL 对 load_file的定义去实现方法似乎已经没有意义。想要解决办法首先需要给 load_file 一个最简单的实现。起码保证不会因为缺少函数实现而报错。紧接着根据具体的报错信息,修改 load_file 函数,一步一步实现满足 Navicat 的需求。这是解决问题的一个思路,一步一步暴露问题层级,逐步分治。基于这种想法,首先给 TiDB 的 load_file 一个空实现,看看效果如何。
相关源码位于 expression/builtin_string.go 中。可以看到 load_file 的实现中抛出了一个 FunctionNotExists 的 error。
type loadFileFunctionClass struct {
baseFunctionClass
}
func (c *loadFileFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
return nil, errFunctionNotExists.GenWithStackByArgs("FUNCTION", "load_file")
}
参考其他字符串函数的实现逻辑,照猫画虎给 load_file 一个空实现。
type loadFileFunctionClass struct {
baseFunctionClass
}
func (c *loadFileFunctionClass) getFunction(ctx sessionctx.Context, args []Expression) (builtinFunc, error) {
if err := c.verifyArgs(args); err != nil {
return nil, err
}
bf, err := newBaseBuiltinFuncWithTp(ctx, c.funcName, args, types.ETString, types.ETString)
if err != nil {
return nil, err
}
bf.tp.Charset, bf.tp.Collate = ctx.GetSessionVars().GetCharsetInfo()
bf.tp.Flen = 64
sig := &builtinLoadFileSig{bf}
return sig, nil
}
type builtinLoadFileSig struct {
baseBuiltinFunc
}
func (b *builtinLoadFileSig) evalString(row chunk.Row) (d string, isNull bool, err error) {
d, isNull, err = b.args[0].EvalString(b.ctx, row)
if isNull || err != nil {
return d, isNull, err
}
return "", true, nil
}
func (b *builtinLoadFileSig) Clone() builtinFunc {
newSig := &builtinLoadFileSig{}
newSig.cloneFrom(&b.baseBuiltinFunc)
return newSig
}
通过几十行模式化的代码,给了 load_file 一个空实现。也就是说再去调用 load_file 时,直接返回 null 结果。
保存这些修改,本地源码编译启动 TiDB。
首先测试单独使用load_file时的结果。结果满足预期。
mysql> select load_file('/tmp/test/test.frm');
+---------------------------------+
| load_file('/tmp/test/test.frm') |
+---------------------------------+
| NULL |
+---------------------------------+
1 row in set (0.00 sec)
接着测试当使用 Navicat 连接 TIDB 然后编辑视图时会发生什么样的问题。
令人惊喜的是,经过这样简单的修改,Navicat 不仅不会再报 load_file not found 的错误,并且编辑视图的功能已经能够正常使用。
效果如下图,右键编辑视图不报错并且正常显示 sql editor 栏的 sql 语句。
四、结语
这次通过抓包发现 TiDB 对 Navicat 兼容性问题并通过大胆假设,小心印证的策略。通过 load_file 的空实现兼容了Navicat 编辑视图功能。由此也可以猜测,Navicat 连接到 TiDB 并编辑视图时,获取表结构的操作并不是必要的,或者也有可能 TIDB 通过其他的方式将表结构信息返回给 Navicat。从而导致 load_file 这一步操作即使是返回 null 也能够保障编辑视图功能的正常工作。
笔者经过手动测试,发现编辑视图功能确实已经修复。就给 load_file 方法的实现加上了单元测试代码。一并提交 PR (https://github.com/pingcap/tidb/pull/28216)到 tidb 的 github 仓库。并在前不久已经合并到 master 分支。但 load_file 毕竟有原本的函数功能定义,目前给出的空实现仅是为了兼容 Navicat。后续既要满足 Navicat 编辑视图不报错,也要实现 load_file 的实际功能,需要更多开发者的不断摸索和实践。