我给Apache顶级项目提了个Bug(一)
这篇文章记录了给 Apache 顶级项目 - 分库分表中间件 ShardingSphere 提交 Bug 的历程。
说实话,这是一次比较曲折的 Bug 跟踪之旅。10月28日,我们在 GitHub 上提交 issue,中途因为官方开发者的主观臆断被 Close 了两次,直到 11 月 20 日才被认定成 Bug 并发出修复版本,历时 20 多天。
本文将还原该 Bug 的分析过程,将有价值的经验和技术点进行提炼。通过本文,你将收获到:
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 进行了分表。
下面是那张数据表的详细定义,共 16 个字段,大家关注前两个字段即可,其他字段和本文提到的 Bug 无关。
前两个字段的作用如下:
•BIZ_DT:业务字段,date类型,和Bug有关
•ECIF_CUST_NO:bigint 类型,用做分区键
代码就是 Controller 调用 Service,Service 调用 Dao,Dao 通过 MyBatis 实现,这里就不粘贴了。
由于使用了 ShardingProxy 中间件,因此它跟直连数据库的配置会有所不同,在定义 dataSource 时,url 需要配置成这样:
jdbc:mysql://127.0.0.1:3307/sharding_db?useServerPrepStmts=true&cachePrepStmts=true&serverTimezone=UTC
可以看到,jdbc 连接的是 ShardingProxy 的逻辑数据源 sharding_db,端口使用的是 3307,并非真正的底层数据库以及 MySQL Server 的真实端口 3306,具体原理下文会介绍到。其中,标蓝色的 useServerPrepStmts 和 cachePrepStmts 这两个参数,和本文说的 Bug 有关,这里先提一下,后面会具体分析。
另外,ShardingProxy 的分表策略是:用 long 类型的 ecif_cust_no 字段对 2 进行取模,分成了两张表。具体配置如下:
shardingColumn: ecif_cust_no
algorithmExpression: pscst_prdt_cvr${ecif_cust_no % 2}
2. 问题描述
再说下遇到的问题。首先,往数据表中预先插入一条 ECIF_CUST_NO 等于 10000 的数据:
然后启动 demo 程序,使用 curl 发起 post 请求,查询 ecifCustNo 等于 10000 的那条记录,居然查询不出数据:
至此,背景基本交代清楚了,为什么数据库中明明有数据,但是程序却查询不出来呢?问题到底出现在 ShardingProxy,还是应用程序本身?
02 ShardingProxy 原理简介
在开启这个问题的分析过程之前,我先快速普及下 ShardingProxy 的基本原理,以便大家能更好的理解我的分析思路。
开源的数据库中间件大家一定接触过,最流行的是 MyCat 和 ShardingSphere。其中 MyCat 是阿里开源的;ShardingSphere 是由当当网开源,并在京东逐渐发展壮大,于 2020 年成为了 Apache 顶级项目。
ShardingSphere 的目标是一个生态圈,它由非常著名的 ShardingJDBC、ShardingProxy、ShardingSidecar 3 款独立的产品组成。本文重点普及下 ShardingProxy,另外两个就不展开了。
1. 什么是 ShardingProxy ?
ShardingProxy 属于和 MyCat 对标的产品,定位为透明化的数据库代理端,可以理解成:一个实现了 MySQL 协议的 Server(独立进程),可用于读写分离、分库分表、柔性事务等场景。
对于应用程序或者 DBA 来说,可以把 ShardingProxy 当做数据库代理,能用 MySQL 客户端工具(Navicat)或者命令行和它直接交互,而 ShardingProxy 内部则通过 MySQL 原生协议与真实的 MySQL 服务器通信。
图1:ShardingProxy 的应用架构图
从架构图来看,ShardingProxy 就相当于 MySQL,它本身不存储数据,但是对外屏蔽了 Database 的存储细节,你可以用连接 MySQL 的方式去连接 ShardingProxy(除了端口不同),用你熟悉的 ORMapping 框架使用它。
2. ShardingProxy 的内部架构
再来看下 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、执行并返回结果
但是很多情况下,一条 SQL 语句可能会反复执行,只是执行时的参数值不同。而预编译功能将这些值用占位符代替,最终达到一次编译、多次运行的效果,省去了解析优化等过程,能大大提高 SQL 的执行效率。
假设我们要执行下面这条 SQL 两次:
SELECT * FROM t_user WHERE user_id = 10;
那 JDBC 和 MySQL 之间的协议消息如下:
通过上述流程可以看到:第 1 条消息是PreparedStatement,查询语句中的参数值用问号代替了,它告诉 MySQL 对这个SQL 进行预编译;第 2 条消息 MySQL 告诉 JDBC 准备成功了;第 3 条消息 JDBC 将参数设置为 1 ;第 4 条消息 MySQL 返回查询结果;第 5 条和第 6 条消息表示第二次执行同样的 SQL,已经无需再次预编译了。
再回到 ShardingProxy,如果需要支持预编译功能,交互流程肯定是需要变的,因为 Proxy 在收到 JDBC 的PreparedStatement 命令时,SQL 里的分片键是问号,它根本不知道该路由到哪个真实数据库。
因此,流程变成了下面这样:
可以看到,Proxy在收到 PreparedStatement 命令后,并不会把这条消息转发给MySQL,只是缓存了这个 SQL,在收到 ExecuteStatement 命令后,才根据分片键和传过来的参数值确定真实的数据库,并与 MySQL 交互。
公众号:铭毅天下Elasticsearch