乐趣区

关于数据库:ShardingSphereProxy-前端协议问题排查方法及案例

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());
    }
}

代码链接:https://github.com/apache/shardingsphere/blob/d928165ea4f6ecf2983b2a3a8670ff66ffe63647/shardingsphere-db-protocol/shardingsphere-db-protocol-mysql/src/main/java/org/apache/shardingsphere/db/protocol/mysql/packet/command/MySQLCommandPacketTypeLoader.java#L38

联合 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 << 23);
        String sql = "insert into foo (id, str0) values (" + id + ",'" + str0 + "')";
        System.out.println(sql.length());
        statement.execute(sql);
    }
}

执行报错如下:

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 << 26);
        String sql = "insert into foo (id, str0) values (" + id + ",'" + str0 + "')";
        statement.execute(sql);
    }
}

产生谬误:

剖析抓包后果:

抓包结果显示,客户端发送了多个 16 MB 的数据包。Wireshark 没能失常解析出 MySQL 的超长数据包,但咱们能够通过搜寻功能定位到 MySQL 的 Packet Header。

再联合 ShardingSphere-Proxy MySQL 的解码逻辑:

int payloadLength = in.markReaderIndex().readUnsignedMediumLE();
int remainPayloadLength = SEQUENCE_LENGTH + payloadLength;
if (in.readableBytes() < remainPayloadLength) {in.resetReaderIndex();
    return;
}
out.add(in.readRetainedSlice(SEQUENCE_LENGTH + payloadLength));

问题根本明确:因为 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

[9]https://github.com/apache/shardingsphere/blob/2c9936497214b8a654cb56d43583f62cd7a6b76b/shardingsphere-proxy/shardingsphere-proxy-frontend/shardingsphere-proxy-frontend-core/src/main/java/org/apache/shardingsphere/proxy/frontend/netty/ServerHandlerInitializer.java

[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 爱好者一起交换。

退出移动版