0
1
1
0
专栏/.../

用 tcpdump 分析 Java 客户端的 prepare 行为

 pepezzzz  发表于  2025-03-17

一、背景

有实际的生产案例中,经常会出现应用开发等数据库用户认为的数据库行为和数据库的实际行为不一致的情况,这时通过网络抓包分析就是非常有效的方案。

本文用 tcpdump 和 wireshark 工具来复现和分析 Java 客户端的 prepare 操作在不同配置下与数据库交互过程中的不同行为。

二、操作过程

  • JAVA DEMO 代码
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Main {
    public static void main(String[] args) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://172.16.201.122:4500/test?characterEncoding=utf8&useSSL=false&...&rewriteBatchedStatements=true");
        config.setUsername("user1");
        config.setPassword("pass1");
        config.setMaximumPoolSize(1);
        config.setPoolName("hikariCP");
        config.setConnectionTimeout(30000);
        config.setMaxLifetime(600000);
        config.setKeepaliveTime(120000);
        HikariDataSource dataSource = new HikariDataSource(config);

        try (Connection connection = dataSource.getConnection()) {
            for (int id = 1; id <= 10; id++) {
                String query = "SELECT * FROM t10 WHERE id = ?";
                try (PreparedStatement statement = connection.prepareStatement(query)) {
                    statement.setInt(1, id);
                    ResultSet resultSet = statement.executeQuery();
                    while (resultSet.next()) {
                System.out.println("id: " + resultSet.getInt("id") + ", name: " + resultSet.getString("name"));
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            dataSource.close();
        }
    }
}
  • 数据库环境初始化
create user user1 identified by 'pass1';
grant all on test.* to user1;
create table test.t10 (id int primary key,name varchar(20));
insert into test.t10 values (1,'a'),(2,'a'),(3,'a'),(4,'a'),(5,'a'),(6,'a'),(7,'a'),(8,'a');

  • 开始抓包并使用 -w 保存为文件,执行结束后使用 ctrl-c 退出。

在本例中,客户端 IP 固定为 host 172.16.201.85 ,数据库服务端端口固定为 port 4500,两者结合组成监听条件。

[root@vm172-16-201-122 ~]# sudo tcpdump -i eth0  host 172.16.201.85 and port 4500 -w 85_4500_3
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^C47 packets captured
55 packets received by filter
0 packets dropped by kernel

  • 为防止抓包无内容,可以再开一个窗口同时将 -w 换成 -vv 查看实时信息输出。
sudo tcpdump -i eth0  host 172.16.201.85 and port 4500 -vv

  • 启用 Java 代码
[root@vm172-16-201-85 hikari]# pwd
/root/hikari
[root@vm172-16-201-85 hikari]# ls
HikariCP-4.0.3.jar  Main.class  Main.java  mysql-connector-java-5.1.49-bin.jar  slf4j-api-1.7.36.jar  slf4j-simple-1.7.36.jar
[root@vm172-16-201-85 hikari]# export CLASSPATH=./HikariCP-4.0.3.jar:./mysql-connector-java-5.1.49-bin.jar:./slf4j-api-1.7.36.jar:./slf4j-simple-1.7.36.jar:.;java -Xms512m  -Xmx512m Main

[root@vm172-16-201-85 hikari]# javac Main.java
[root@vm172-16-201-85 hikari]# java Main
[main] INFO com.zaxxer.hikari.HikariDataSource - hikariCP - Starting...
[main] INFO com.zaxxer.hikari.HikariDataSource - hikariCP - Start completed.
id: 1, name: a
id: 2, name: a
id: 3, name: a
id: 4, name: a
id: 5, name: a
id: 6, name: a
id: 7, name: a
id: 8, name: a
[main] INFO com.zaxxer.hikari.HikariDataSource - hikariCP - Shutdown initiated...
[main] INFO com.zaxxer.hikari.HikariDataSource - hikariCP - Shutdown completed.

  • 使用 wireshark 分析结果

wireshare 打开文件后,选择 4500 即 数据库的端口的通讯条目,右键选择 decode as ...

image.png

将 4500 端口的通讯都解析为 mysql 协议,MySQL 协议内容转变为可见。

image.png

同时,可以用 tcpdump 查询语句 mysql.query contains "关键字",查询关键 SQL 语句所在的位置。

image.png

三、分析结果

开启客户端句柄缓存

开启 useServerPrepStmts = true 后同时配置 cachePrepStmts = true,这会让客户端缓存预处理语句。

config.setJdbcUrl("jdbc:mysql://172.16.201.122:4500/test?characterEncoding=utf8&useSSL=false&useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSqlLimit=10000&prepStmtCacheSize=1000&useConfigs=maxPerformance&rewriteBatchedStatements=true");

每次的执行过程是 Prepare statement + 多次 Execute statement。

  • Prepare

客户端发送 prepare 语句请求

返回客户端 prepare 语句句柄和参数定义

  • {Execute statement}*10

后续过程客户端发送语句句柄和参数

返回客户端 Execute 语句结果

image.png

关闭客户端句柄缓存

useServerPrepStmts = true 能让服务端执行预处理语句,但默认情况下客户端每次执行完后会 close 预处理语句,并不会复用。

删除 &cachePrepStmts=true&prepStmtCacheSqlLimit=10000&prepStmtCacheSize=1000&useConfigs=maxPerformance

注意:需要一并删除 useConfigs=maxPerformance,它会配置多个参数,其中也包括 cachePrepStmts = true。

config.setJdbcUrl("jdbc:mysql://172.16.201.122:4500/test?characterEncoding=utf8&useSSL=false&useServerPrepStmts=true&rewriteBatchedStatements=true");

每次的执行过程是 Prepare statement + Execute statement + Close statement。

  • Prepare statement

  • Execute statement

  • Close statement

服务端行为对比

对比以上两种交互方式的数据库服务端监控与抓包分析的现象一致。

第一时间段 07:42 ,开启客户端缓存。第二时间段 07:45 ,关闭客户端缓存。

Prepare 阶段 第二时间段数量高

Execute 阶段 两个时间段数量一致

Close 阶段 第二时间段数量高

第二阶段未能使用 plan cache (默认情况下 plan cache 在执行 close 后会释放)

可用的改善方法:服务端启用 tidb_ignore_prepared_cache_close_stmt 保留缓存。

启用后会忽略关闭 Prepared Statement 的指令,包括 Binary 协议的 COM_STMT_CLOSE 信号和文本协议的 DEALLOCATE PREPARE 语句都会被忽略。

下图中,第一次未启用 tidb_ignore_prepared_cache_close_stmt,第二次已启用。启用后,能实现在收到 close 后,下一条 SQL 仍能够命中服务端缓存。

TiDB 内部基于 SQL 语句的文本计算 hash 值,在没有客户端句柄的情况下,通过匹配 hash 命中 Plan Cache。代码位置如:https://github.com/pingcap/tidb/blob/67edd7d8f73de399bd72490d449d1dede1ee637b/pkg/planner/core/plan_cache_utils.go#L261

关闭服务端缓存

一旦使用 useServerPrepStmts=false(无论客户端是否缓存句柄),客户端就会关闭使用 plan cache 的行为,所有语句使用 Query 接口。

config.setJdbcUrl("jdbc:mysql://172.16.201.122:4500/test?characterEncoding=utf8&useSSL=false&useServerPrepStmts=false");
  • Query statement

  • 服务端行为

使用 Query 无 plancache

四、总结

Java 的行为结果如下表:

配置

SQL 交互

JDBC 配置

服务端行为

启用客户端缓存

Prepare statement

{Execute statement}*10

&useServerPrepStmts=true

&cachePrepStmts=true

&prepStmtCacheSqlLimit=10000

&prepStmtCacheSize=1000

&useConfigs=maxPerformance

正常使用 plan cache

客户端发送 Execute 时指定句柄

关闭客户端缓存

{Prepare statement

Execute statement

Close statement}*10

删除

&cachePrepStmts=true

&prepStmtCacheSqlLimit=10000

&prepStmtCacheSize=1000

&useConfigs=maxPerformance

不能使用 plan cache(被 close 掉)

tidb_ignore_prepared_cache_close_stmt

可以保留缓存。

关闭服务端缓存

{query}*10

useServerPrepStmts=false

使用 Query 接口(无 plan cache)

同时可以参考文档: https://docs.pingcap.com/zh/tidb/stable/dev-guide-connection-parameters/

某 JAVA 应用的交易压测测试成绩:

配置项

成绩

说明

&useServerPrepStmts=true&

cachePrepStmts=true&

prepStmtCacheSqlLimit=10000&prepStmtCacheSize=1000

104ms

利用客户端缓存实现单次 prepare 多次 Execute。

Execute 使用服务端 plan cache 性能最好。

&useServerPrepStmts=false&

cachePrepStmts=true&

prepStmtCacheSqlLimit=10000&prepStmtCacheSize=1000

117ms

不使用服务端 plan cache,与客户端缓存配置无关。

直接使用 query 接口,性能中等。

&useServerPrepStmts=true

139ms

未利用客户端缓存多次 prepare、Execute 和 close,

导致网络交互过多。

close 后 Execute 未使用服务端 plan cache,

需要再次解析,性能最差。

&useServerPrepStmts=false

118ms

直接使用 query 接口,交互方式和性能同2。

通过本例,可以看出 tcpdump + wireshark 的过程可以有效地找出预期不一致时,实际双方是怎么交互的,对于故障分析有非常大的帮助。

0
1
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论