写点什么

从抓包发现并解决 Navicat 编辑 TiDB 视图报错的问题

  • 2022 年 7 月 11 日
  • 本文字数:3090 字

    阅读完需:约 10 分钟

作者: leeray 原文来源:https://tidb.net/blog/57768a45


【是否原创】是


【首发渠道】知乎 - 神州数码云基地


【首发渠道链接】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 的实际功能,需要更多开发者的不断摸索和实践。


发布于: 刚刚阅读数: 3
用户头像

TiDB 社区官网:https://tidb.net/ 2021.12.15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
从抓包发现并解决 Navicat 编辑 TiDB 视图报错的问题_实践案例_TiDB 社区干货传送门_InfoQ写作社区