ShardingSphere-Proxy 前端协议问题排查方法及案例
来源:SegmentFault
时间:2023-02-16 15:16:50 198浏览 收藏
怎么入门数据库编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《ShardingSphere-Proxy 前端协议问题排查方法及案例》,涉及到MySQL、数据库、shardingsphere,有需要的可以收藏一下
ShardingSphere-Proxy 是 Apache ShardingSphere 的接入端之一,其定位为透明化的数据库代理。ShardingSphere-Proxy 实现了数据库协议,理论上可以被任何使用或兼容 MySQL / PostgreSQL / openGauss 协议的客户端访问。相比 ShardingSphere-JDBC,ShardingSphere-Proxy 的优势在于对异构语言的支持,以及为 DBA 提供数据库集群的可操作入口。
与 ShardingSphere 的 SQL 解析模块相似,ShardingSphere-Proxy 对数据库协议的支持度也是一个长期积累的过程,需要开发者不断去完善 ShardingSphere-Proxy 的数据库协议实现。
本篇将给大家介绍数据库协议开发过程中常用的工具,并以一次 ShardingSphere-Proxy MySQL 协议问题的排查过程作为本文工具使用的案例。
1 使用 Wireshark 分析网络协议
Wireshark 是一个常用的网络协议分析工具,其内置了对数百种协议的解析支持(包括本文相关的 MySQL / PostgreSQL 协议),能够读取多种不同类型的抓包格式。
Wireshark 的完整功能、安装等内容可以参考 Wireshark 官方文档[1]。
1.1 使用 Wireshark 或 tcpdump 等工具抓包
1.1.1 使用 Wireshark 抓包
Wireshark 本身具备抓包能力,如果连接 ShardingSphere-Proxy 的环境可以运行 Wireshark,可以直接使用 Wireshark 抓包。
Wireshark 启动后,首先选择正确的网卡。
以本地运行 ShardingSphere-Proxy 为例,客户端通过
127.0.0.1端口
3307连接 ShardingSphere-Proxy,流量都经过 Loopback 网卡,因此选择 Loopback 作为抓包对象。
选择网卡后,Wireshark 即开始抓包。由于网卡中可能会有很多其他进程的流量,需要过滤出指定端口的流量:
tcp.port == 3307`
1.1.2 使用 tcpdump 抓包
在 ShardingSphere-Proxy 部署在线上环境,或其他我们无法使用 Wireshark 抓包的情况下,可以考虑使用 tcpdump 或其他工具抓包。
对网卡
eth0抓包,过滤 TCP 端口 3307,并将抓包结果写入到
/path/to/dump.cap。示例命令:
tcpdump -i eth0 -w /path/to/dump.cap tcp port 3307
tcpdump 的使用方式可以通过 man tcpdump 查看,本文不再赘述。tcpdump 的抓包结果文件可以通过 Wireshark 打开。
1.1.3 抓包注意事项
客户端连接 MySQL,可能会自动启用 SSL 加密,抓包结果无法直接解析出协议内容。使用 MySQL 命令行客户端可以指定参数禁用 SSL,命令如下:
mysql --ssl-mode=disable
使用 JDBC 可以增加参数,参数如下:
jdbc:mysql://127.0.0.1:3306/db?useSSL=false
1.2 使用 Wireshark 读取抓包内容
Wireshark 支持读取多种抓包文件格式,包括 tcpdump 的抓包格式。
Wireshark 默认会把
3306端口解码 MySQL 协议、
5432端口解码为 PostgreSQL 协议。对于 ShardingSphere-Proxy 可能使用不同端口的情况,可以在
Decode As...中为指定端口配置协议。
例如,ShardingSphere-Proxy MySQL 使用了
3307端口,可以按照以下步骤把
3307端口解码为 MySQL 协议:
当 Wirekshark 能够解析出 MySQL 协议后,我们可以增加过滤条件,只显示 MySQL 协议数据:
tcp.port == 3307 and mysql
为指定端口选择正确的协议后,可以在 Wireshark 窗口看到协议的内容。
示例,客户端与服务端建立 TCP 连接后,MySQL 服务端主动向客户端发送 Greeting,协议如下图所示:
示例,客户端执行 SQL select version(),协议如下图所示:
2 协议问题排查案例——让 ShardingSphere-Proxy MySQL 支持超长数据包
2.1 问题描述
使用 MySQL Connector/J 8.0.28 作为客户端连接 ShardingSphere-Proxy 5.1.1 执行批量插入报错,更换驱动 MySQL Connector/J 5.1.38 后问题解决。
[INFO ] 2022-05-21 17:32:22.375 [main] o.a.s.p.i.BootstrapInitializer - Database name is `MySQL`, version is `8.0.28` [INFO ] 2022-05-21 17:32:22.670 [main] o.a.s.p.frontend.ShardingSphereProxy - ShardingSphere-Proxy start success [ERROR] 2022-05-21 17:37:57.925 [Connection-143-ThreadExecutor] o.a.s.p.f.c.CommandExecutorTask - Exception occur: java.lang.IllegalArgumentException: Sequence ID of MySQL command packet must be `0`. at com.google.common.base.Preconditions.checkArgument(Preconditions.java:142) at org.apache.shardingsphere.db.protocol.mysql.packet.command.MySQLCommandPacketTypeLoader.getCommandPacketType(MySQLCommandPacketTypeLoader.java:38) at org.apache.shardingsphere.proxy.frontend.mysql.command.MySQLCommandExecuteEngine.getCommandPacketType(MySQLCommandExecuteEngine.java:50) at org.apache.shardingsphere.proxy.frontend.mysql.command.MySQLCommandExecuteEngine.getCommandPacketType(MySQLCommandExecuteEngine.java:46) at org.apache.shardingsphere.proxy.frontend.command.CommandExecutorTask.executeCommand(CommandExecutorTask.java:95) at org.apache.shardingsphere.proxy.frontend.command.CommandExecutorTask.run(CommandExecutorTask.java:72) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834) [ERROR] 2022-05-21 17:44:24.926 [Connection-317-ThreadExecutor] o.a.s.p.f.c.CommandExecutorTask - Exception occur: java.lang.IllegalArgumentException: Sequence ID of MySQL command packet must be `0`. at com.google.common.base.Preconditions.checkArgument(Preconditions.java:142) at org.apache.shardingsphere.db.protocol.mysql.packet.command.MySQLCommandPacketTypeLoader.getCommandPacketType(MySQLCommandPacketTypeLoader.java:38) at org.apache.shardingsphere.proxy.frontend.mysql.command.MySQLCommandExecuteEngine.getCommandPacketType(MySQLCommandExecuteEngine.java:50) at org.apache.shardingsphere.proxy.frontend.mysql.command.MySQLCommandExecuteEngine.getCommandPacketType(MySQLCommandExecuteEngine.java:46) at org.apache.shardingsphere.proxy.frontend.command.CommandExecutorTask.executeCommand(CommandExecutorTask.java:95) at org.apache.shardingsphere.proxy.frontend.command.CommandExecutorTask.run(CommandExecutorTask.java:72) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834)
2.2 详细排查过程
报错处于 Proxy 前端,可以排除后端 JDBC Driver 的原因,与协议实现有关。
2.2.1 问题分析
源码中直接判断如果 sequence ID 不等于 0 就直接报错。
public final class MySQLCommandPacketTypeLoader { /** * Get command packet type. * * @param payload packet payload for MySQL * @return command packet type for MySQL */ public static MySQLCommandPacketType getCommandPacketType(final MySQLPacketPayload payload) { Preconditions.checkArgument(0 == payload.readInt1(), "Sequence ID of MySQL command packet must be `0`."); return MySQLCommandPacketType.valueOf(payload.readInt1()); } }
结合 MySQL 协议文档,考虑什么情况下 sequence ID 会不等于 0[2]:
- 服务端响应多个消息给客户端;
- 客户端发送多个连续的消息;
- ……
其中,MySQL Packet 的消息头由 3 字节长度 + 1 字节 Sequence ID 组成[3],因此 Payload 部分最大长度为 16 MB - 1。结合问题描述,报错是在批量插入的时候产生,考虑问题可能是:客户端发送的数据超过单个 MySQL Packet 的长度上限,拆分为多个连续的 MySQL Packet,但 Proxy 无法处理。
2.2.2 尝试重现问题
使用
longtext类型字段。本来设想的是构造一条长度超过 16 MB 的 SQL。但无意中发现,SQL 长度超过 8 MB 的情况下也报错了,复现代码如下:
try (Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:13306/bmsql", "root", "root")) { try (Statement statement = connection.createStatement()) { statement.execute("drop table if exists foo"); statement.execute("create table foo (id bigint primary key, str0 longtext)"); long id = ThreadLocalRandom.current().nextLong(); String str0 = RandomStringUtils.randomAlphanumeric(1
执行报错如下:
Wireshark 抓包结果显示,数据包长度 0x80003C == 8388668,只有一个 MySQL Packet,sequence ID 也只有 0,如下图:
调试代码发现,Proxy 所使用的 readMediumLE() 方法是有符号数,Packet 长度读取为负数了。
该问题比较好修复,更换对应的
readUnsignedMediumLE()方法即可。
虽然该问题报错信息和问题描述的现象一致,但还没有解决根本问题。
长度溢出问题修复后,继续排查问题。使用以下代码,向 ShardingSphere-Proxy 发送约 64 MB 的数据:
try (Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:13306/bmsql", "root", "root")) { try (Statement statement = connection.createStatement()) { statement.execute("drop table if exists foo"); statement.execute("create table foo (id bigint primary key, str0 longtext)"); long id = ThreadLocalRandom.current().nextLong(); String str0 = RandomStringUtils.randomAlphanumeric(1
发生错误:
分析抓包结果:
抓包结果显示,客户端发送了多个 16 MB 的数据包。Wireshark 没能正常解析出 MySQL 的超长数据包,但我们可以通过搜索功能定位到 MySQL 的 Packet Header。
再结合 ShardingSphere-Proxy MySQL 的解码逻辑:
int payloadLength = in.markReaderIndex().readUnsignedMediumLE(); int remainPayloadLength = SEQUENCE_LENGTH + payloadLength; if (in.readableBytes()
问题基本明确:由于 ShardingSphere-Proxy 没有对数据包进行聚合,多个数据包被 Proxy 当成多个命令分别解析,由于后续数据包 Sequence ID 大于 0,Proxy 内部对 Sequence ID 的断言逻辑报错。
2.3 排查及修复
经排查,报错原因为:
- (直接原因)ShardingSphere-Proxy MySQL 协议解包逻辑没有正确处理长度符号[4];
- (根本原因)ShardingSphere-Proxy MySQL 没有对超过 16 MB 的数据包进行聚合[5]。
首先要了解 MySQL 协议对超长数据包的处理方式[6]:
- 当数据总长度超过 16 MB - 1,协议会将数据拆分为多个长度为 16 MB - 1 的 Packet,直到最后数据长度小于 16 MB - 1,如下图所示:
- 当数据长度恰好等于 16 MB - 1 或其倍数,在发送一个或多个长度为 16 MB - 1 的数据包后,还会接一个长度为 0 的数据包,如下图所示:
解决思路:为了让 ShardingSphere-Proxy MySQL 的协议实现能够不关心超长数据包的处理方式,决定在数据解码逻辑对数据包进行聚合。
在 ShardingSphere-Proxy 的前端 Netty 解码逻辑中,当遇到长度为 0xFFFFFF 的数据 Packet,则通过 CompositeByteBuf 对多个 MySQL Packet 的 Payload 部分进行聚合。
具体代码见参考文档的 Pull Request。
目前已修复以下问题:
- 正确处理数据包长度数值符号[7];
- MySQL 协议解码逻辑支持超过 16 MB 数据包[8]。
后续需要解决的潜在问题,包括但不限于:
- MySQL 协议编码逻辑不支持响应超过 16 MB 的数据包。
3 ShardingSphere-Proxy 前端协议排查方法总结
对于协议类问题排查,首先需要熟悉对应的协议,熟悉数据库协议的方式包括但不限于:
- 通过抓包工具观察客户端直接连接数据库的协议;
- 根据数据库协议文档;
- 阅读数据库官方客户端(例如 JDBC Driver)的协议编码逻辑源码。
基本掌握抓包工具与协议后,就可以开始排查 ShardingSphere-Proxy 前端协议的问题了。
ShardingSphere-Proxy 与客户端建立连接的代码入口在
org.apache.shardingsphere.proxy.frontend.netty.ServerHandlerInitializer
[9],可以以此作为起点排查问题。
另外,本文案例中问题的修复已随 Apache ShardingSphere 5.1.2 发布[10],欢迎大家下载使用!
参考文档
[1] https://www.wireshark.org/
[2] https://dev.mysql.com/doc/internals/en/sequence-id.html
[3] https://dev.mysql.com/doc/internals/en/mysql-packet.html
[4] https://github.com/apache/shardingsphere/issues/17891
[5] https://github.com/apache/shardingsphere/issues/17907
[6] https://dev.mysql.com/doc/internals/en/sending-more-than-16mbyte.html
[7] https://github.com/apache/shardingsphere/pull/17898
[8] https://github.com/apache/shardingsphere/pull/17914
[10] https://shardingsphere.apache.org/document/current/cn/downloads/
作者
吴伟杰,Apache ShardingSphere PMC,SphereEx 基础设施研发工程师。专注于 Apache ShardingSphere 接入端及 ShardingSphere 子项目 ElasticJob 的研发。
欢迎点击链接,了解更多内容:
Apache ShardingSphere 官网:https://shardingsphere.apache.org/
Apache ShardingSphere GitHub 地址:https://github.com/apache/shardingsphere
SphereEx 官网:https://www.sphere-ex.com
欢迎添加社区经理微信(ss_assistant_1)加入交流群,与众多 ShardingSphere 爱好者一同交流。
今天关于《ShardingSphere-Proxy 前端协议问题排查方法及案例》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于mysql的内容请关注golang学习网公众号!
-
499 收藏
-
244 收藏
-
235 收藏
-
157 收藏
-
101 收藏
-
208 收藏
-
174 收藏
-
317 收藏
-
371 收藏
-
244 收藏
-
288 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习