浅析MySQL协议从一个bug谈起

36次阅读

共计 3269 个字符,预计需要花费 9 分钟才能阅读完成。

在经过一系列紧锣密鼓的筹备后,Sharding-Sphere 3.0.0.M2 终于在 2018.8.8 正式跟大家见面了。发版之前我们解决了几个棘手的问题,今天拓海与大家分享其中一个:MySQL 协议相关的一个 bug。在 bug 的定位过程中,小伙伴们会了解到一些 MySQL 协议的基本知识和调试方法。


Bug 描述

关于 bug 的描述,这里使用了项目的 issue 模板,大家在提 bug 的时候也请一定遵循这个模板。在本文中使用中文描述,但大家提 issue 的时候记得用英文,毕竟老外也会来看的。

Which version of Sharding-Sphere do you using?
3.0.0.M2-SNAPSHOT

Which project do you using? Sharding-JDBC or Sharding-Proxy?
Sharding-Proxy

Expected behavior
持续正常的 INSERT 数据。

Actual behavior
循环插入 200 万数据,当插入到 100 万左右的时候,客户端无响应。

Reason analyze
客户端无响应可能是 Proxy 阻塞引起的,也可能是 Proxy 导致客户端阻塞引起的。

Steps to reproduce the behavior
持续插入数据到 100 万条以上即可复现。

Bug 初步定位

经过初步分析,Proxy 日志正常,MySQL 里也能查到用户插入的最后一条数据。那么,问题应该就出在最后一条数据插入成功之后。可能有两种情况:1. 插入数据成功后,MySQL 返回的消息被 Proxy 错误解码,导致 Proxy 异常;2. Proxy 返回给客户端的消息编码错误,导致客户端异常。

第一种情况,如果 Proxy 解码错误,Netty 会抛异常,TCP 连接断开,Proxy 会把异常打印到日志,所以这种情况可能性不大。第二种情况可能性较大,需要分析 Proxy 和客户端之间的消息交互,进一步定位。

MySQL 协议

准确的说应该是 MySQL Client/Server 协议,另一个叫 X Protocol 的暂不涉及。任何 MySQL 客户端(CLI、GUI、MySQL 驱动)与 MySQL 服务器通信,都需要使用这个协议。这个协议包含一系列的命令消息(COM)和响应消息(Response)、编解码方式和交互流程。

上图展示了绝大多数协议消息的交互过程:对于 DQL,服务器会返回 FIELD_COUNT(列数)、FIELD(列的描述信息,如类型等)、ROW(每一行数据的值)、EOF(结束标志);而对于 DML,只需要返回 OK 或者 ERR,通知客户端命令执行成功或失败。

经过之前的分析,我们可以把范围缩小到 OK_Packet 这条响应消息上,这条消息由服务器发送给客户端,表示一条命令的成功执行。也就是说,在插入数据成功后,MySQL 服务器会发送 OK_Packet 给 Proxy,后者又会把这条消息发送给客户端。消息格式如下:

这个表格看起来有点抽象,我们可以先用 Wireshark 抓个真实包看看,解析出来会直观一些:

做协议开发永远离不开 Wireshark,后面拓海会手把手教你使用 Wireshark 进行网络抓包。真实的数据看起来就简单明了一些了,有包长度、包序号、影响行数、最后插入 id、服务器状态等信息。我们找一个字段,来说明其编解码方式。例如 last_insert_id,它的类型是 int<lenenc>,值为 1083。

MySQL 协议里存储数据的方式是小端字节序,这是什么意思呢?比如一个值 0x1234,采用大端字节序的存储方式就是 0x1234,而小端方式则是 0x3412。MySQL 采用这样的字节序很不友好,对于一个通信协议,应该使用网络字节序传输数据,而不是机器字节序。

回到协议,int<lenenc> 是变长编码的整数类型,不同范围的数值使用不同的长度来编码:

由上可知,1083 这个值应该是 0xfc 加上 2 个字节来表示:

1083 = 0x043b,小端字节序就是 0x3b04。有了这些基础,我们就可以通过抓包来分析产生 bug 的根本原因了。

Bug 最终定位

以下就是当时抓到的包,看起来非常的“正常”,插入一条数据,返回一条成功的响应,Wireshark 也没有解析出错。看现象就是客户端停止了插入数据。

经过多次试验,最后一条 INSERT 总是卡在 t_order_item 这个表上,它与 t_order 表有什么本质区别呢?了解 Sharding-Sphere-Example 的同学对这两个表应该很熟悉,t_order 使用的是雪花算法生成的分布式主键,值很大,而 t_order_item 使用的是 MySQL 自增主键。为什么主键值小的表会卡住?

又经过一系列调查,发现 t_order_item 表也不是每次都卡住,这个表有两个分片,在其中一个分片上永远没问题,而另一个分片上只要插入数据就会卡。这两个表又有什么区别呢?其实,由于分片的不均匀,一个表的数据量多,另一个数据量少,除此之外没什么不同。数据量多的表大概 6 万多条数据。

在拓海百思不得其解的时候,亮神和赵帅看出了些端倪:

最后一条 OK_Packet 的 Last INSERT ID 值太大了,一个表不可能有这么多数据。通过观察发现,表的数据量已经大于 65535,应该使用 3 个字节存储

然而程序里却使用了四个字节。四个字节的编码在协议里是不存在的,这必然导致客户端 jdbc 解析出错。至于为什么 wireshark 没有解析出错,那一定是因为它的解析逻辑错了。

使用 Wireshark 分析 MySQL 协议

Wireshark 可以理解为是一个带图形界面的 tcpdump,同时集成了非常多的协议解析插件,可以帮助我们方便的抓取和分析 MySQL 协议。需要注意的是,Wireshark 只能抓网络协议包,但 MySQL 协议还有其他通信方式,如 Shared memory,Named pipes,这些是抓不到的,最好不要开这些参数。

Linux 命令行环境下: 可以先用 tcpdump 抓包,然后将包发送到本地,使用 Wireshark 解析。命令如下:

tcpdump -i any port 3307 -w test.pcap

any: 表示所有网络接口。

port: tcp 端口,Proxy 的默认端口是 3307,如果你关心 MySQL 的包,就改为 3306。

pcap: 一种通用的数据流格式,Wireshark 可以直接打开。

用 Wireshark 打开 test.pcap,你会发现除了 MySQL 协议,还显示很多 TCP 协议的 ACK 消息:

在过滤器中输入 mysql,进行过滤即可:

Windows 环境下 :直接使用 Wireshark 抓包,一边抓包一边解析。本地的测试环境往往会同时运行客户端、Proxy 和 MySQL 服务器,它们的通信在同一台机器上是不走网卡的,然而 windows 系统又没有提供本地回环网络的接口,所以这种情况下是什么也抓不到的。

为了解决这个问题,我们需要把 Wireshark 在 Windows 系统上默认的抓包工具 WinPcap 替换为 Npcap,在这里下载:https://nmap.org/npcap/#download

安装前先在 Wireshark 的安装目录下卸载 WinPcap。安装时勾选如下选项:

安装完成再次启动 Wireshark 时,就会看到在网络接口列表中,多了一项 Npcap Loopback adapter,这个就是来抓本地回环包的网络接口了:

小结

Sharding-Sphere 自 2016 开源以来,不断精进、不断发展,被越来越多的企业和个人认可:在 Github 上收获 5000+ 的 star,1900+forks,60+ 的各大公司企业使用它,为 Sharding-Sphere 提供了重要的成功案例。此外,越来越多的企业伙伴和个人也加入到 Sharding-Sphere 的开源项目中,为它的成长和发展贡献了巨大力量。

我们从未停息过脚步,聆听社区伙伴的需求和建议,不断开发新的强大功能,不断使其健壮可靠!开源不易, 我们却愿向着最终的目标,步履不停!那么,正在阅读的你,是否可以助我们一臂之力呢?分享、转发、使用、交流,以及加入我们,都是对我们最大的鼓励!

正文完
 0