关于 JDBC 驱动程序的性能。

当第一版 MariaDB Java Client 发布时,有人在评论中询问了该驱动程序与 ConnectorJ 相比的性能特性。我用手势回答说,没有人会做愚蠢的事情,驱动程序的性能应该大致相同,但我承诺总有一天会测量它并告诉全世界。现在,这一天到来了。在这一天,我们将对比三个 MySQL JDBC 驱动程序(ConnectorJ、MariaDB JDBC 和 Drizzle JDBC)的性能。与服务器一直受到基准测试的关注不同,连接器没有标准基准测试,所以我需要即兴发挥,同时尽量保持服务器的开销最小。所以我一开始做了一些非常原始的事情。我使用了我最喜欢的两个查询:

  • DO 1 — 这个不返回结果集,因此可以看作是一个小的“更新”。
  • SELECT 1 — 最小的 SELECT 查询。

测试程序会运行查询 N 次,如果查询是 SELECT,它会使用 ResultSet.getObject(i) 检索结果集中的所有值,并计算每秒查询次数(QPS)。(最棒的是测试程序是单线程的,你多久能运行一次单线程测试呢?🙂 测试是在我自己的工作站上运行的,运行的是 Windows Server 2008 R2,我在 ConnectorJ 的 URL 中设置了 useConfigs=maxPerformance。)

结果(每秒查询次数,未预处理)

ConnectorJ-5.1.24 MariaDB-JDBC-1.1.2 Drizzle-JDBC-1.3-SNAPSHOT
DO 1 19543 22104 15288
SELECT 1 17004 19305 13410

jdbc_fast_queries

 

MariaDB JDBC 看起来比 ConnectorJ 稍快(约 10%),比 Drizzle JDBC 快得多(约 30%)。

ConnectorJ 能做得更好吗?我敢肯定可以。通过分析器输出(NetBeans 中的 CPU 分析、插桩模式)查看循环执行“SELECT 1”的测试,显示 com.mysql.jdbc.StatementImpl.findStartOfStatement() 占用了 7.5% 的运行时。当然,插桩结果应该谨慎看待,但是使用字符串搜索的唯一原因是——如果在 ResultSet.executeQuery() 中执行更新 (DML) 语句,它会抛出异常而被拒绝。我相信这可以有不同的做法。如果绝对必要,可以延迟抛出异常,直到客户端发现服务器发送的是 OK 包而不是结果集。

更有趣的是 Drizzle JDBC 的情况。理论上,由于 MariaDB 驱动程序继承了 Drizzle JDBC 的血统,性能特性应该相似,但事实并非如此,因此某处肯定有 bug。它似乎很容易找到,根据分析器,50.2% 的 CPU 时间(这个数字要大打折扣地看待)花费在一个从字节缓冲区构造 hexdump 的函数中。查看源代码,我们发现下面这行代码是无条件执行的:

log.finest("Sending : " + MySQLProtocol.hexdump(byteHeader, 0));

虽然 hexdump 的结果从未使用过(除非日志级别是 FINEST),但仍会创建 dump 字符串,使用了相对昂贵的 Formatter 例程,并与字符串“Sending:”连接,然后被丢弃……替 Markus 辩护一下,hexdump() 不是他的错误,它是在 3 年前贡献的。但它在 3 年内一直未被发现。这个 bug 现已提交到 https://github.com/krummas/DrizzleJDBC/issues/21 [更新:这个 bug 在报告后的几小时内就解决了]

那么,让我们看看将这段有问题的代码放入 if (log.getLevel() == java.util.logging.Level.FINEST) 条件中能获得多少提升。
“DO 1”的 QPS 从 15288 提高到 19968 (30%),而“SELECT 1”从 13410 提高到可观的 16824 (25%)。对于一行代码的修复来说,这很不错了。
jdbc_fast_queries_drizzle_fix

虽然这行代码使得 Drizzle JDBC 变得更快,甚至比 ConnectorJ 的数字稍好,但仍然不如 MariaDB 快。

在 MariaDB JDBC 连接器中,自从分叉以来进行了一些性能改进。早期的一项改进是避免在发送数据时进行不必要的复制,并减少字节缓冲区的数量。最近又有一项改进,是在分析后发现解析 Field 数据包的开销很大(主要是因为构造列名、别名等字符串......)。这项改进采用了延迟解析,推迟了字符串的构造,并在大多数情况下完全避免了它。例如,如果不使用列名,而是通过整数索引在 ResultSet.getXXX(int i) 中访问行,则不会完全解析元数据。此外,也许还有一些我记不清的其他修复。🙂

我们能否进一步提高 QPS?

我们可以试试。首先,语句可以进行预处理。MariaDB 和 Drizzle 目前只提供客户端预处理语句(ConnectorJ 可以同时进行客户端和服务器端预处理语句),但使用它们可以节省将查询转换为字节以及 JDBC 转义预处理的开销。从现在开始,我只测试被证明是最快的查询“DO 1”。在 MariaDB 驱动程序上测试显示 QPS 有一些微小的提升,从 22104(未预处理)到 22183(预处理),提升了 0.3%。在 ConnectorJ 上提升稍多(19543 vs 20096,或 2.9%)。到目前为止没有什么革命性的发现。

但是,我们还没有用尽所有选项来最大化“DO 1”的性能(虽然这有点傻)。回想一下,ConnectorJ 支持 Windows 上的命名管道,据说这比 TCP 连接快得多。使用命名管道重启服务器,将 JDBC URL 设置为“jdbc:mysql:///?socketFactory=com.mysql.jdbc.NamedPipeSocketFactory&namedPipePath=\\.\Pipe\MySQL&user=root&useConfigs=maxPerformance”,然后用 100 万次预处理查询重新运行测试。现在 QPS 增长到了 29542!这个数字很强劲,与迄今为止最好的结果相比提升了 33%。然而,不幸的是,仍然没有成功,因为当命名管道连接关闭时,JVM 会抛出堆栈跟踪。这是一个“不修复”的 MySQL bug Bug#62518(被归咎于 JVM 问题),这使得命名管道支持几乎无用——尽管在这种情况下可能有什么技巧可以阻止 JVM 抛出错误,但我并不知道这样的技巧。

C 客户端库相比之下有多快?

出于好奇,我也测试了原生客户端与 JDBC 的比较。使用 TCP 协议时,它比最快的 JDBC(MariaDB,预处理)稍好一些,但差距不大——24063 QPS vs 22183(相差 8.5%),我相信 Java 驱动程序还可以进一步改进。
使用命名管道时,QPS 为 33122,这比 ConnectorJ 在命名管道正常工作时能达到的性能好约 12%。

 

访问基准测试程序

我将基准测试程序连同驱动程序一起放在了 Launchpad 上。如果你使用 Windows,并且服务器运行在端口 3306,并且‘root’用户没有密码,你可以直接分支代码库并运行 bench_all.bat。对于使用其他操作系统的用户,我相信你们能够快速将批处理文件重写为 shell 脚本。