这篇文章记录了给 Apache 顶级项目 - 分库分表中间件 ShardingSphere 提交 Bug 的历程。
说实话,这是一次比较曲折的 Bug 跟踪之旅。10月28日,我们在 GitHub 上提交 issue,中途因为官方开发者的主观臆断被 Close 了两次,直到 11 月 20 日才被认定成 Bug 并发出修复版本,历时 20 多天。
1、疑难问题的排查思路
2、数据库中间件 Sharding Proxy 的原理
3、MySQL 预编译的流程和交互协议
4、Wireshark 抓包分析 MySQL 的奇淫技巧
01 问题描述
这个 Bug 来源于我的公号读者,他替公司预研 ShardingProxy(属于 ShardingSphere 的子产品,可用作分库分表,后文会详细介绍)。他按照官方文档写了一个很简单的 demo,但是运行后无法查询出数据。
下面是他遇到问题后发给我的信息,希望我能帮忙一起定位下原因。
截图中的 doc 详细记录了 ShardingProxy 的配置、调试分析日志、以及问题的具体现象。
为了方便大家理解,我重新描述下这个 Demo 的业务逻辑以及问题表象。
1. Demo 的业务逻辑说明
这个 Demo 很简单,主要为了跑通 ShardingProxy 的分库分表功能。程序用 SpringBoot + MyBatis 实现了一个单表的查询逻辑,然后用这张表的一个 long 类型字段作为分区键,并通过 ShardingProxy 进行了分表。
前两个字段的作用如下:
-
BIZ_DT:业务字段,date类型,和Bug有关 -
ECIF_CUST_NO:bigint 类型,用做分区键
jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
shardingColumn: ecif_cust_no
algorithmExpression: pscst_prdt_cvr${ecif_cust_no % 2}
2. 问题描述
然后启动 demo 程序,使用 curl 发起 post 请求,查询 ecifCustNo 等于 10000 的那条记录,居然查询不出数据:
至此,背景基本交代清楚了,为什么数据库中明明有数据,但是程序却查询不出来呢?问题到底出现在 ShardingProxy,还是应用程序本身?
02 ShardingProxy 原理简介
ShardingSphere 的目标是一个生态圈,它由非常著名的 ShardingJDBC、ShardingProxy、ShardingSidecar 3 款独立的产品组成。本文重点普及下 ShardingProxy,另外两个就不展开了。
ShardingProxy 属于和 MyCat 对标的产品,定位为透明化的数据库代理端,可以理解成:一个实现了 MySQL 协议的 Server(独立进程),可用于读写分离、分库分表、柔性事务等场景。
对于应用程序或者 DBA 来说,可以把 ShardingProxy 当做数据库代理,能用 MySQL 客户端工具(Navicat)或者命令行和它直接交互,而 ShardingProxy 内部则通过 MySQL 原生协议与真实的 MySQL 服务器通信。
图1:ShardingProxy 的应用架构图
从架构图来看,ShardingProxy 就相当于 MySQL,它本身不存储数据,但是对外屏蔽了 Database 的存储细节,你可以用连接 MySQL 的方式去连接 ShardingProxy(除了端口不同),用你熟悉的 ORMapping 框架使用它。
再来看下 ShardingProxy 的内部架构,后续源码分析时会涉及到此部分。
图2:ShardingProxy 的内部架构图
整个架构分为前端、核心组件和后端:
前端(Frontend)负责与客户端进行网络通信,采用的是 NIO 框架,在通信的过程中完成对MySQL协议的编解码。
核心组件(Core-module)得到解码的 MySQL 命令后,开始调用 Sharding-Core 对 SQL 进行解析、改写、路由、归并等核心功能。
后端(Backend)与真实数据库交互,采用 Hikari 连接池,同样涉及到 MySQL 协议的编解码。
3. ShardingProxy 的预编译 SQL 功能
本文的 Bug 跟 ShardingProxy 的预编译 SQL 有关,这里单独介绍下此功能以及与之相关的 MySQL 协议,这个是本文的关键,请耐心看完。
熟悉数据库开发的同学一定了解:预编译 SQL(PreparedStatement),在数据库收到一条 SQL 到执行完毕,一般分为以下 3 步:
1、词法和语义解析
2、优化 SQL,制定执行计划
3、执行并返回结果
SELECT * FROM t_user WHERE user_id = 10;
03 问题分析
因为整个代码很简单,代码层面唯一有可能存在问题的是 Mybatis 这一层。为了确认这一点,我修改了 SpringBoot 的配置,将 MyBatis 的 debug 日志也打印了出来。再次发起请求后,能从控制台中看到以下详细日志:
jdbc:mysql://127.0.0.1:3306/db1?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
通过这一步,我将怀疑对象再次转移到 ShardingProxy 上了,并将 dataSource 配置改回成原样,继续排查。
首先,查看 ShardingProxy 的运行日志,没发现任何异常;其次,能看到日志中的 Actual SQL 是正确的,它已经根据分区键正确路由到了 pcsct_prdt_cvr0 这张表:
[INFO ] 17:25:48.804 [ShardingSphere-Command-15]
ShardingSphere-SQL - Actual SQL: ds_0 ::: SELECT
BIZ_DT,ECIF_CUST_NO,DEP_FLG ...
FROM pscst_prdt_cvr0
WHERE ECIF_CUST_NO = ? ::: [10000]
我开始怀疑:是否跟 ShardingProxy 所使用的数据库驱动有关?因为这个 Jar 包是应用方选择版本,手动放到 ShardingProxy 安装目录中的。因此,我将驱动版本从 5.1.47 版本改成了 8.0.13 (和 Demo 使用了相同的版本),但是问题仍然存在。
另外,还能想到的是:是否是 ShardingProxy 的这个最新版本引入了 Bug?然后,我又另外安装了它的上一个版本 4.1.0,重新测试了一遍,还是有问题。
这个时候,真感觉没有其他可疑点了,所有能想到的点都排查了一遍。我再次回到了 Demo 程序本身,它和 ShardingProxy 唯一的结合点就在 DataSource 的 url 上。
jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
找到这个问题的解决方案后,我同步给了读者。与此同时,他也在 ShardingProxy 的 GitHub 上提交了 issue,反馈了这个最新进展。
由于工作原因,这个问题我就暂时放一边了,准备抽空再接着排查。
大概过了一周我想起了这个问题,然后打开 issue 想了解下调查进度,让我非常惊讶的是:官方开发者居然在复现此问题后,主观臆断地认为是应用程序的问题,然后莫名奇妙的把这个 issue 关闭了,他们的答复是这样的:
mysql || tcp.port==3307
04 根本原因定位
当天晚上,官方开发者就定位到了根本原因,发出了 Pull Request。我看了下代码改动,仅仅修改了一行代码。
1、为什么代码抛异常了,但是 ShardingProxy 的控制台没打印呢?
2、为什么 ShardingProxy 需要做 date 到 Timestamp 的类型转换呢?
简单理解就是:ShardingProxy 在代码实现时,用了一个范围最大的 timestamp 存了三种可能的值 date, datetime 和 timestamp,然后再按照上面这个协议规范进行二进制的写入。
3、这个 Bug 是只有在使用 SQL 预编译功能时才会被触发吗?
05 写在最后
本文详细复盘了这个 Bug 的分析过程,并对其中的原理知识和排查经验进行了总结。
对于 ShardingSphere 这种顶级开源项目来说,我个人觉得同样值得做一次深度复盘。我不认同他们对于 issue 的处理方式,另外在核心功能的自动化测试上,也一定是存在 case 不完善的,不然不可能连续多个版本都没发现这个严重 Bug。
最近写的一些干货,每篇都很用心,欢迎各位小伙伴阅读/点赞/分享:
我是Guide哥,Java后端开发,会一点前端知识,喜欢烹饪,自由的少年。一个三观比主角还正的技术人。我们下期再见!
文章评论