我给 PostgreSQL 官方 JDBC 驱动修复了一个高并发性能问题

这是我在 2022 年给 PostgreSQL 官方 JDBC 驱动 修复的一个高并发性能问题。

该问题影响的版本范围是 pgjdbc:

  • 42.3.2
  • 42.3.3

Issue: Concurrent performance issue in 42.3.2 caused by #2291 https://github.com/pgjdbc/pgjdbc/issues/2450
PR: Use non-synchronized getTimeZone in TimestampUtils #2451 https://github.com/pgjdbc/pgjdbc/pull/2451

在 ShardingSphere-Proxy 峰值性能测试中发现问题

这个问题是我在对 ShardingSphere-Proxy + PostgreSQL 进行性能测试的过程中发现的。

性能测试过程中,发现 ShardingSphere-Proxy CPU 使用率及实时峰值 TPS 比前两天的测试有所下降,遂在测试过程中使用 async-profiler 对 JVM 进行采样。
采样时设置了 --lock 门槛为 1us,以 jfr 格式导出采样信息。

采样执行了不到 5 分钟,使用 IDEA 打开 jfr 文件,发现在 TimeZone 类调用上有大量的 ObjectMonitor(也就是 synchronized 代码块发生了多线程竞争)。
在这里插入图片描述
Monitor Blocked 事件数量庞大,5 分钟达到百万级,说明 synchronized 多线程竞争激烈。
在这里插入图片描述

也可以用 Java Mission Control 打开 jfr 文件进行分析。

采样过程不足 5 分钟,但 Total Block Time 却高达 3.4 小时,说明大量线程受 synchronized 影响。

为什么 Total Block Time 会高于实际采样时长?
Total Block Time 是所有线程等待时长的和。
举个例子:
假如有一个 synchronized 代码块,临界区内代码需要运行 5 分钟。目前有线程 A、B、C 同时尝试进入 synchronized 代码块,A 成功进入临界区,线程 B、C 则需要等待 5 分钟后才可能进入临界区。如果在 A 进入临界区前开始采样,并在 A 离开临界区后结束采样,此时的 Total Block Time 就是线程 B、C 等待时间的和,即 10 分钟。

在这里插入图片描述

从采样结果看,synchronized 的代码路径在 PostgreSQL JDBC 驱动内,有可能是这段时间有人调整了 pgjdbc 的驱动。

检索 ShardingSphere 这段时间的提交记录,发现有个 PR 升级了 pgjdbc 驱动,从版本 42.2.5 升级到了 42.3.2。

[issue-15271] upgrade postgres driver #15272

更换回 42.2.5 版本驱动后,ShardingSphere-Proxy PostgreSQL 性能表现恢复了。

深入探究 pgjdbc

跟踪社区反馈及代码变更

pgjdbc 是 PostgreSQL 的官方 Java 驱动,出现这样的性能问题,影响可能会非常广泛。

于是,我到 pgjdbc 检索是否有相关问题反馈或解决方案。

在 pgjdbc GitHub 仓库中搜索了相关性能问题,没有找到类似的情况,于是拉了代码开始跟踪变更。

由于性能问题与 TimestampUtil 相关,我就查找了所有和此类相关的变更,发现了这一改动:
fix: use local TimestampUtil in PgStatement and PgResultset for thread safety #2291

PR 说明为解决线程安全问题,把原本由 Connection 持有的 TimestampUtil,改为由 Statement 和 ResultSet 分别持有。
new TimestampUtil 的执行频率增加了。

考虑到使用连接池的情况下,Connection 确实存在多线程非并发访问的情况,不排查线程安全问题的风险。

由于该问题在当时还没有其他人反馈,笔者准备修复这一问题。

直接修复问题

最新的 pgjdbc 要求的 Java 版本为 1.8,所以这个问题修复很简单,换一个获取 UTC 时区的方法就行:
PR: Use non-synchronized getTimeZone in TimestampUtils #2451 https://github.com/pgjdbc/pgjdbc/pull/2451
在这里插入图片描述

修复前:

private final TimeZone utcTz = TimeZone.getTimeZone("UTC");

修复后:

private final TimeZone utcTz = TimeZone.getTimeZone(ZoneOffset.UTC);

编写针对问题的 JMH 测试用例并运行测试

PostgreSQL JDBC 驱动为了确保性能,在仓库中维护了一定的 JMH 测试用例。在大致看了下 pgjdbc 仓库内的 JMH 测试后,发现 timestamp 相关的测试用例并没有覆盖并发性能。
于是,我这边写了一个针对 setTimestamp 并发性能的测试,使用 JMH 内嵌的基于 JMX 实现的 StackProfiler 进行基本的线程状态收集统计。
需要注意的是,测试逻辑中只有执行 setTimestamp,并没有调用 PreparedStatement 的执行方法,就是不实际执行 SQL。

因此,数据库性能与本次性能测试无关,且期望测试期间进程的 CPU 使用率为接近 100% 用户态。

完整源码在 issue 中有记录:https://github.com/pgjdbc/pgjdbc/issues/2450

@State(Scope.Thread)
@Warmup(iterations = 5, time = 3)
@Measurement(iterations = 5, time = 3)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class PgPreparedStatementBenchmark {private final Connection connection;private final ThreadLocalRandom random = ThreadLocalRandom.current();private PreparedStatement preparedStatement;public PgPreparedStatementBenchmark() {Connection connection;try {connection = DriverManager.getConnection("jdbc:postgresql://127.0.0.1:5432/postgres", "postgres", "postgres");} catch (SQLException e) {connection = null;e.printStackTrace();}this.connection = connection;}@Setup(Level.Invocation)public void setup() {try {preparedStatement = connection.prepareStatement("select ?");} catch (SQLException e) {e.printStackTrace();}}@Benchmarkpublic void benchSetTimestamp() throws SQLException {preparedStatement.setTimestamp(1, new Timestamp(random.nextLong(Long.MAX_VALUE)));}@TearDown(Level.Invocation)public void tearDown() {try {preparedStatement.close();} catch (SQLException e) {e.printStackTrace();}}public static void main(String[] args) throws RunnerException {new Runner(new OptionsBuilder().include(PgPreparedStatementBenchmark.class.getName()).threads(Runtime.getRuntime().availableProcessors()).forks(3).addProfiler(StackProfiler.class).build()).run();}
}

JMH 测试环境与参数

测试环境为 2 路 12C 24T CPU 组成的共 24C 48T,故 JMH 使用 48 线程测试。

# JMH version: 1.33
# VM version: JDK 1.8.0_312, OpenJDK 64-Bit Server VM, 25.312-b07
# VM invoker: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-1.el7_9.x86_64/jre/bin/java
# VM options: -Dfile.encoding=UTF-8
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 3 s each
# Measurement: 5 iterations, 3 s each
# Timeout: 10 min per iteration
# Threads: 48 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp

JMH 结果分析

代码修复前测试结果

Stack profiler 给出了线程状态分布,发现 BLOCKED 状态占比超过 95%。

Result "icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp":1790.346 ±(99.9%) 138.368 ops/ms [Average](min, avg, max) = (1587.859, 1790.346, 2024.605), stdev = 129.430CI (99.9%): [1651.977, 1928.714] (assumes normal distribution)Secondary result "icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp:·stack":
Stack profiler:....[Thread state distributions]....................................................................95.4%         BLOCKED2.2%         RUNNABLE2.0%         TIMED_WAITING0.4%         WAITING....[Thread state: BLOCKED].........................................................................95.4% 100.0% java.util.TimeZone.getTimeZone....[Thread state: RUNNABLE]........................................................................2.0%  93.9% java.util.TimeZone.getTimeZone0.0%   1.2% java.util.GregorianCalendar.computeFields0.0%   0.6% org.openjdk.jmh.util.Deduplicator.dedup0.0%   0.4% sun.util.calendar.Gregorian.newCalendarDate0.0%   0.3% java.util.Calendar.<init>0.0%   0.3% org.postgresql.jdbc.PgConnection.prepareStatement0.0%   0.3% org.postgresql.jdbc.TimestampUtils.<init>0.0%   0.2% java.util.TimeZone.clone0.0%   0.2% org.postgresql.core.v3.SimpleParameterList.<init>0.0%   0.2% java.util.Arrays.copyOf0.0%   2.3% <other>....[Thread state: TIMED_WAITING]...................................................................2.0% 100.0% java.lang.Object.wait....[Thread state: WAITING].........................................................................0.4% 100.0% sun.misc.Unsafe.park

代码修复后测试结果

根据线程状态分布,代码修复后测试显示不存在 BLOCKED 状态,TIMED_WAITING 与 WAITING 状态可能与 JMH 测试线程同步相关,基本可以认定线程一直处于 RUNNABLE 状态。

Result "icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp":11152.920 ±(99.9%) 572.247 ops/ms [Average](min, avg, max) = (10385.107, 11152.920, 12101.528), stdev = 535.280CI (99.9%): [10580.673, 11725.167] (assumes normal distribution)Secondary result "icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp:·stack":
Stack profiler:....[Thread state distributions]....................................................................97.3%         RUNNABLE2.0%         TIMED_WAITING0.6%         WAITING....[Thread state: RUNNABLE]........................................................................33.8%  34.7% sun.util.calendar.ZoneInfo.getTransitionIndex10.6%  10.8% java.util.HashMap.hash8.1%   8.3% java.util.GregorianCalendar.computeFields4.2%   4.4% org.postgresql.jdbc.TimestampUtils.appendTime4.0%   4.1% java.util.Calendar.<init>2.8%   2.9% org.postgresql.jdbc.TimestampUtils.<init>2.5%   2.6% org.postgresql.jdbc.PgConnection.prepareStatement2.4%   2.5% icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.tearDown2.4%   2.4% icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp2.2%   2.3% org.postgresql.jdbc.TimestampUtils.toString24.4%  25.0% <other>....[Thread state: TIMED_WAITING]...................................................................2.0% 100.0% java.lang.Object.wait....[Thread state: WAITING].........................................................................0.6% 100.0% sun.misc.Unsafe.park

JDK 源码分析

为什么换个方法性能就能恢复了?直接看 java.util.TimeZone 的源码就知道了。

TimeZone.getTimeZone 有两种方法签名:

  • 第一个是从 JDK 早期版本就存在的方法,接受 String 作为参数;
  • 第二个是从 1.8 起加入的方法,接受 java.time.ZoneId(也是从 1.8 起提供的 class)作为参数。

从源码中看到,接受 String 参数的老方法使用了 synchronized 修饰,而新的方法及其依赖的方法均没有 synchronized 修饰。

https://github.com/openjdk/jdk/blob/ec0cc6300a02dd92b25d9072b8b3859dab583bbd/src/java.base/share/classes/java/util/TimeZone.java#L536-L570
在这里插入图片描述
public 的 getTimeZone 方法所依赖的 private getTimeZone 方法并没有复杂的逻辑,也没有 synchronized 同步。

https://github.com/openjdk/jdk/blob/ec0cc6300a02dd92b25d9072b8b3859dab583bbd/src/java.base/share/classes/java/util/TimeZone.java#L608-L617
在这里插入图片描述

java.time.ZoneOffsetjava.time.ZoneId 的子类,其中维护了一个 UTC 常量,可以直接用于 getTimeZone

https://github.com/openjdk/jdk/blob/ec0cc6300a02dd92b25d9072b8b3859dab583bbd/src/java.base/share/classes/java/time/ZoneOffset.java#L155
在这里插入图片描述

即更换了新的 getTimeZone 方法后,能完全避免 synchronized

性能修复随 pgjdbc 42.3.4 版本发布

https://github.com/pgjdbc/pgjdbc/releases/tag/REL42.3.4

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/67291.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微软杀入Web3:打造基于区块链的AI产品

作者&#xff1a;秦晋 2023年1月&#xff0c;微软向 ChatGPT 创建者 OpenAI 投资 100 亿美元&#xff0c;在AI业界引发格外关注。此举也让微软在AI的战略探索上提前取得有利位置。 2023年3月&#xff0c;微软软件工程师 Albacore 披露微软正在为Edge 浏览器测试内置的非托管加密…

深入理解索引B+树的基本原理

目录 1. 引言 2. 为什么要使用索引&#xff1f; 3. 索引的概述 4. 索引的优点是什么&#xff1f; 4.1 降低数据库的IO成本&#xff0c;提高数据查找效率 4.2 保证数据库每一行数据的唯一性 4.3 加速表与表之间的连接 4.4 减少查询中分组与排序的执行时间 5. 索引的缺点…

微信小程序实现左滑删除

一、效果 二、代码 实现思路使用的是官方提供的 movable-area&#xff1a;注意点&#xff0c;需要设置其高度&#xff0c;否则会出现列表内容重叠的现象。由于movable-view需要向右移动&#xff0c;左滑的时候给删除控件展示的空间&#xff0c;故 movable-area 需要左移 left:…

静态网页和动态网页区别

1&#xff0c;静态网页和动态网页有何区别 1) 更新和维护 静态网页内容一经发布到网站服务器上&#xff0c;无论是否有用户访问&#xff0c;这些网页内容都是保存在网站服务器上的。如果要修改网页的内容&#xff0c;就必须修改其源文件&#xff0c;然后重新上传到服务器上。…

【论文阅读】NoDoze:使用自动来源分类对抗威胁警报疲劳(NDSS-2019)

NODOZE: Combatting Threat Alert Fatigue with Automated Provenance Triage 伊利诺伊大学芝加哥分校 Hassan W U, Guo S, Li D, et al. Nodoze: Combatting threat alert fatigue with automated provenance triage[C]//network and distributed systems security symposium.…

单链表相关操作(插入,删除,查找)

通过上一节我们知道顺序表的优点&#xff1a; 可随机存储&#xff08;O(1)&#xff09;&#xff1a;查找速度快 存储密度高&#xff1a;每个结点只存放数据元素&#xff0c;而单链表除了存放数据元素之外&#xff0c;还需存储指向下一个节点的指针 http://t.csdn.cn/p7OQf …

基于ssm+vue的新能源汽车在线租赁管理系统源码和论文PPT

基于ssmvue的新能源汽车在线租赁管理系统源码和论文PPT010 开发环境&#xff1a; 开发工具&#xff1a;idea 数据库mysql5.7(mysql5.7最佳) 数据库链接工具&#xff1a;navcat,小海豚等 开发技术&#xff1a;java ssm tomcat8.5 摘 要 随着科学技术的飞速发展&#xff0…

完整版:TCP、UDP报文格式

目录 TCP报文格式 报文格式 报文示例 UDP报文格式 报文格式 报文示例 TCP报文格式 报文格式 图1 TCP首部格式 字段长度含义Source Port16比特源端口&#xff0c;标识哪个应用程序发送。Destination Port16比特目的端口&#xff0c;标识哪个应用程序接收。Sequence Numb…

如何在iPhone手机上修改手机定位和模拟导航?

如何在iPhone手机上修改手机定位和模拟导航&#xff1f; English 首先&#xff0c;你需要在Mac电脑上下载安装 Location Simulator/定位模拟工具 和 Runner 这两款应用程序。 完成安装后&#xff0c;打开软件&#xff0c;并用USB连接手机设备 修改iPhone手机定位和模拟导航 …

第二部分:AOP

一、AOP简介 AOP(Aspect Oriented Programming)面向切面编程&#xff0c;一种编程范式&#xff0c;指导开发者如何组织程序结构。 AOP是OOP&#xff08;面向对象编程&#xff09;的进阶版。 作用&#xff1a;在不改变原始设计的基础上为其进行功能增强。 spring理念&#x…

嵌入式Linux驱动开发系列五:Linux系统和HelloWorld

三个问题 了解Hello World程序的执行过程有什么用? 编译和执行&#xff1a;Hello World程序的执行分为两个主要步骤&#xff1a;编译和执行。编译器将源代码转换为可执行文件&#xff0c;然后计算机执行该文件并输出相应的结果。了解这个过程可以帮助我们理解如何将代码转化…

TIOBE2023年8月榜单发布,Python超越老将C/C++蝉联冠军

TIOBE 编程社区指数是一个衡量编程语言受欢迎程度的指标&#xff0c;评判的依据来自世界范围内的工程师、课程、供应商及搜索引擎&#xff0c;TIOBE 官网近日公布了 2023 年 8 月的编程语言排行榜。 此次的榜单中&#xff0c;Python依旧稳居第一&#xff0c;占比达到了13.33%。…