Wireshark网络分析之追踪TiDB官方Bug

383 阅读6分钟

问题描述

在我们的小程序首页,用户点击不同的区域,因不同的区域是有不同的内容,但诡异的是返回了同样的信息。

技术实现,包括前后端的逻辑都是非常简单的:后端的不同区域分配的id值不同,前端传入参数带上这个id,以便得到不同的结果。

发生问题之后,我们将注意力集中到后端具体的请求接口,最终的逻辑就是一句SQL语句

SELECT * FROM tbl_challenge WHERE (id=?)

这条语句真的是简单的不能再简单了:根据id,返回不同的记录

明明输入的id=1和id=2,但结果都返回了id=1的记录。

奇哉怪哉。

问题分析

解决该问题之前,首先要做的就是复现该问题

我们写了一个单元测试,立马复现,代码如下:

func TestTiDBQueryId(t *testing.T) {
   t.Run("test query id", func(t *testing.T) {
      var info1 = &TblCropInfo{}
      selectSql := `SELECT * FROM tbl_crop_info WHERE (id=?)`
      err := libs.GetMysqlDb(DressUpDb).Raw(selectSql, 2).QueryRow(&info1)
      assert.Nil(t, err)

      var info2 = &TblCropInfo{}
      selectSql = `SELECT * FROM tbl_crop_info WHERE (id=?)`
      err = libs.GetMysqlDb(DressUpDb).Raw(selectSql, 4).QueryRow(&info2)
      assert.Nil(t, err)
      assert.Equal(t, info1, info2)
   })
}

结果返回的都是id=2的数据

那么好,既然复现了。我们需要确定这个问题到底是我们client的问题,还是TiDB服务器的问题。

考虑到TiDB已经被广泛应用,我们首先怀疑自己的代码或者依赖的包有问题。

因为业务逻辑非常简单,很快排除了代码的问题。

那如何确定第三方client包的问题?

第一种方法就是运行代码,进行Debug,一步一步追踪代码的逻辑

优点是「代码之下,毫无秘密」,缺点是代码较为复杂、层层叠叠,很容易迷失在调用的过程中。

不过幸好,我们还好第二种方法。

第二种方法就是直接抓包。看看我们发出去的请求包中的id值是否为想要赋予的值。

最终确定通过wireshark抓包的方式进行问题的追踪。(因为我自己也比较拿手

抓包分析

我们的问题在于:代码中给定不同的id,却返回了同一条数据记录

实验的目的是验证:我们的client发送给TiDB server两个请求中id值是否相等。

为此,我们将这一任务分成两部分:首先抓到请求包,然后对抓包的包进行分析。

进行抓包

  • 根据目的IP,确定网卡接口

为此,我们需要route命令,本质上是查看路由表。根据目的ip,路由表会告诉我们发出信息会经过本机的哪一个网卡接口.

比如我们想要抓取请求百度的包,那它经过哪个网卡呢?

🐂🍺  ~  route -n get www.baidu.com

route to: 110.242.68.4
destination: default
mask: default
gateway: 192.168.10.1
interface: en0
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING>
recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire
0 0 0 0 0 0 1500 0

没错,如上所展示的,经过en0。那么由此可以确定,wireshark主界面应该选择en0接口,抓取到百度的包。

确定送请求到TiDB server的网卡信息,类似上面百度的例子,只需要我们将TiDB serverip地址替换掉www.baidu.com即可。

  • 进行抓包,然后运行代码,得到抓包文件。
// 代码类似如下
func TestTiDBQueryId(t *testing.T) {
    t.Run("test query id", func(t *testing.T) {
    var info1 = &TblCropInfo{}
    selectSql := `SELECT * FROM tbl_challenge WHERE (id=?)`
    err := libs.GetMysqlDb(DressUpDb).Raw(selectSql, 2).QueryRow(&info1)
    assert.Nil(t, err)

    var info2 = &TblCropInfo{}
    selectSql = `SELECT * FROM tbl_challenge WHERE (id=?)`
    err = libs.GetMysqlDb(DressUpDb).Raw(selectSql, 4).QueryRow(&info2)
    assert.Nil(t, err)
    assert.Equal(t, info1, info2)
    })
}

分析包

当我们开始分析包的时候,我们需要知道TiDB采用的通信协议。根据官网与 MySQL 兼容性对比的信息:

TiDB 高度兼容 MySQL 5.7 协议、MySQL 5.7 常用的功能及语法。MySQL 5.7 生态中的系统工具(PHPMyAdmin、Navicat、MySQL Workbench、mysqldump、Mydumper/Myloader)、客户端等均适用于 TiDB。

我们得出结论,TiDB采用MySQL作为其应用层协议。

然后开始看包吧。

因为我们的TiDB server采用6033端口提供服务,所以首先我们在过滤器输入tcp.port == 6033,查看执行此次代码得到的相关包。

大致浏览一下,我们期待能在抓包文件看见MySQL协议,结果却没有,但出人意料的是,我们看见了X11协议。

image.png 那么,X11协议是个什么东西?

X11 is version 11 of the X Window System protocol.

我们已知TiDB采用MySQL协议进行数据传输,那么出现的X11可能是解析出错了。

我们查看wireshark官网中,对X11协议的描述:

image.png

没错,wireshark应该是解析错误。

什么,wireshark还会解析出错?

应用层以下,每一层的协议都有专门的字段显示上层协议是什么,具体见本人拙作网络协议中为什么要有协议类型的字段

应用层协议,wireshark基本上靠端口号+其余的一些信息进行判断。所以就存在出错的可能喽。

此时,我们可以按照wireshark官方的提示,将之解析为MySQL协议。

选中一个X11包,右键,继续点击Decode As

image.png

修改Current列为MySQL即可

image.png 点击ok,然后回到主界面,此时发现到处都是MySQL协议啦。

image.png

我们想要找到执行具体SQL时的通信,在过滤执行tcp.port == 6033 and tcp contains "SELECT * FROM tbl_challenge "

image.png

我们发现执行这两句的src-port均为54090,可以确定是在同一个TCP连接中执行了这两个SQL语句。

标记📌这两个SQL预处理包然后Follow TCP Stream

image.png

首先查看第一个select语句发送的id数据

image.png

如我们观察到的,id=1

然后查看第二个select语句,发送的id数据

image.png

我的天,id=30001

也就是说,我们的代码没问题、使用的第三方client包没问题,因为我们真真切切的发送了不同的id值。期望得到不同的记录,结果TiDB server居然给我们返回了相同的数据!

终于找到这次事件的元凶了。

(其实发现client包有问题也没问题,那将是一个金闪闪的PR。)

抓包总结

重要的是明确此次实验的目标:我们需要通过数据包确定,两次select语句传送的id值是否不同

为此,我们首先需要将运行的代码对TiDB server的请求抓取下来。

其次,经过抽丝剥茧,我们需要找到两次select语句,然后确定对应的id值。

最终我们发现,我们发出去的id值是不同的,但是返回的信息是相同的。

问题直指TiDB官方。

问题的解决

解决其实非常简单,我们将该问题以及问题排查过程发送给云服务厂商,他们回复称升级一下TiDB版本就可,后面升级了版本。我们再次验证,发现没有问题。

image.png bingo!

参考

  • 《Wireshark数据包分析实战》

  • TiDB官网

  • Wireshark官网