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

news2024/11/24 19:01:11

这是我在 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();
        }
    }
    
    @Benchmark
    public 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.430
  CI (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%         BLOCKED
  2.2%         RUNNABLE
  2.0%         TIMED_WAITING
  0.4%         WAITING

....[Thread state: BLOCKED].........................................................................
 95.4% 100.0% java.util.TimeZone.getTimeZone

....[Thread state: RUNNABLE]........................................................................
  2.0%  93.9% java.util.TimeZone.getTimeZone
  0.0%   1.2% java.util.GregorianCalendar.computeFields
  0.0%   0.6% org.openjdk.jmh.util.Deduplicator.dedup
  0.0%   0.4% sun.util.calendar.Gregorian.newCalendarDate
  0.0%   0.3% java.util.Calendar.<init>
  0.0%   0.3% org.postgresql.jdbc.PgConnection.prepareStatement
  0.0%   0.3% org.postgresql.jdbc.TimestampUtils.<init>
  0.0%   0.2% java.util.TimeZone.clone
  0.0%   0.2% org.postgresql.core.v3.SimpleParameterList.<init>
  0.0%   0.2% java.util.Arrays.copyOf
  0.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.280
  CI (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%         RUNNABLE
  2.0%         TIMED_WAITING
  0.6%         WAITING

....[Thread state: RUNNABLE]........................................................................
 33.8%  34.7% sun.util.calendar.ZoneInfo.getTransitionIndex
 10.6%  10.8% java.util.HashMap.hash
  8.1%   8.3% java.util.GregorianCalendar.computeFields
  4.2%   4.4% org.postgresql.jdbc.TimestampUtils.appendTime
  4.0%   4.1% java.util.Calendar.<init>
  2.8%   2.9% org.postgresql.jdbc.TimestampUtils.<init>
  2.5%   2.6% org.postgresql.jdbc.PgConnection.prepareStatement
  2.4%   2.5% icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.tearDown
  2.4%   2.4% icu.wwj.jmh.jdbc.PgPreparedStatementBenchmark.benchSetTimestamp
  2.2%   2.3% org.postgresql.jdbc.TimestampUtils.toString
 24.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.coloradmin.cn/o/880241.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Mongodb 5.0]聚合操作

本文对应Aggregation Operations — MongoDB Manual 正文 此章节主要介绍了Aggregation Pipeline&#xff0c;其实就是将若干个聚合操作放在管道中进行执行&#xff0c;每一个聚合操作的结果作为下一个聚合操作的输入&#xff0c;每个聚合指令被称为一个stage。 在正式开始学…

Vue使用jspdf和html2canvas组件库结合导出PDF文件

效果图&#xff1a; 1、安装依赖&#xff1a; npm install html2canvas --save npm install jspdf --save 或 yarn add html2canvas --save yarn add jspdf --save 2、封装全局调用方法&#xff1a;this.$exportPDF(#id,文件名) 新建js文件&#xff1a;/utils/html2Pdf.js&am…

Nginx 服务优化与防盗链

Nginx隐藏版本号、服务名 1. 改配置文件 HTTP模块中添加命令&#xff1a;server_tokens off&#xff1b; 关闭防火墙、安全机制 将配置文件备份一份&#xff0c;再进行配置 2. 改源码包 /opt/nginx-1.22.0/src/core/nginx.h #define NGINX_VERSION "1.1.1" #修改…

SSH公网远程直连Docker容器

文章目录 1. 下载docker镜像2. 安装ssh服务3. 本地局域网测试4. 安装cpolar5. 配置公网访问地址6. SSH公网远程连接测试7.固定连接公网地址8. SSH固定地址连接测试 在某些特殊需求下,我们想ssh直接远程连接docker 容器,下面我们介绍结合cpolar工具实现ssh远程直接连接docker容器…

【BASH】回顾与知识点梳理(二十七)

【BASH】回顾与知识点梳理 二十七 二十七. 磁盘配额(Quota)27.1 磁盘配额 (Quota) 的应用与实作什么是 QuotaQuota 的一般用途Quota 的使用限制Quota 的规范设定项目 27.2 一个 XFS 文件系统的 Quota 实作范例实作 Quota 流程&#xff1a;设定账号实作 Quota 流程-1&#xff1a…

使用 PostgreSQL 创建全文搜索引擎2:Postgres 与 Elasticsearch

使用 PostgreSQL 作为全文搜索引擎很诱人&#xff0c;因为它需要的基础设施较少。但它的搜索相关功能集是否足以与基于 Lucene 的替代方案竞争&#xff1f; 在第 1 部分中&#xff0c;我们深入研究了 PostgreSQL 全文搜索的功能&#xff0c;并探讨了如何实现相关性提升(releva…

如何选择最佳的文件传输协议?(FTP、TFTP、Raysync)

在数字化时代&#xff0c;通过互联网传输文件是一项常见的任务。因此&#xff0c;选择适合您企业需求的文件传输协议非常重要。 文件传输协议是发送方和接收方之间的一套规则和信息。它的作用就像网络两端都能理解的一种语言&#xff0c;使得数据可以正确输出并带有正确的文件…

SRM订单管理:优化供应商关系

一、概述SRM订单管理的概念&#xff1a; SRM订单管理是指在供应商关系管理过程中&#xff0c;有效管理和控制订单的创建、处理和交付。它涉及与供应商之间的沟通、合作和协调&#xff0c;旨在实现订单的准确性、可靠性和及时性。 二、SRM订单管理的流程&#xff1a; 1. 订单创…

问道管理:缩量小幅上涨说明什么?

股市里面&#xff0c;股票价格上涨或跌落都是常见现象。可是关于那些在商场上寻求收益的出资者来说&#xff0c;他们需要对每一个股市中的价格动摇有深化的了解&#xff0c;以便做出更正确的出资决策。最近&#xff0c;出资者们发现商场缩量小幅上涨的现象时有发生&#xff0c;…

SpringBoot复习:(51)默认情况下DataSource是怎么创建出来的,是什么类型的?

DataSource是通过DataSourceAutoConfiguration创建的&#xff0c;这个类代码如下&#xff1a; 可以看到DataSourceAutoConfiguration有个静态内部类PooledDataSourceConfiguration,在这个类上有个Import注解&#xff0c;导入了DataSourceConfiguration.Hikari这个类&#xff0…

8.15号经典模型复习笔记

文章目录 Deep Residual Learning for Image Recognition(CVPR2016)方法 Densely Connected Convolutional Networks&#xff08;CVPR2017&#xff09;方法 EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks&#xff08;ICML2019&#xff09;方法 Re…

科创板四周年,6家半导体厂商被评为最具价值上市公司

7月21日&#xff0c;由聚焦科创板的权威媒体《科创板日报》及财联社联合发起的“科创板开市四周年评选”榜单正式发布&#xff0c;其包括“2023最具价值科创板上市公司”、“2023最具创新力科创板上市公司”等子榜单。 其中&#xff0c;“2023最具价值科创板上市公司”子榜单评…

九州未来参与编制的开源领域3项团体标准获批发布

日前&#xff0c;中电标2023年第21号团体标准公告正式发布&#xff0c;其中由九州未来参与编制的3项开源领域团体标准正式获批发布&#xff0c;于2023年8月1日正式实施。 具体内容如下&#xff1a; 《T/CESA 1269-2023 信息技术 开源 术语与综述》&#xff0c;本文件界定了信息…

point line

2 * (2 - 1) / 2 1 3 * (3 - 1) / 2 3 4 * (4 - 1) / 2 6 5 * (5 - 1) / 2 10 ...... n * (n - 1) / 2

【实用插件】ArcGIS for AutoCAD插件分享下载

ArcGIS包含一系列功能&#xff0c;其中ArcGIS for AutoCAD一个免费的可下载的AutoCAD插件&#xff0c;它可简化将CAD和GIS数据整合在一起的过程提供互操作性。 ArcGIS for AutoCAD互操作性平台将连接AutoCAD和 ArcGIS&#xff0c;以增强使用地理环境设计CAD工程图时的用户体验…

vue3 + ts defineProps 设置默认值 + 子父之间的通信

首先要引入 import { withDefaults, defineProps, defineEmits } from vue1、接受父组件传递的参数与默认值 // 接受父组件传递参数 withDefaults(defineProps<{name: String;}>(),{name: 小怪兽,} ) 2、暴露子组件数据与方法 (如没有暴露&#xff0c;父组件不能使用子…

【C快学-C语言程序设计(基础篇)】从VSCode中使用C编写我的第一个Hello world

简介&#xff1a;本专栏是一个C语言基础入门知识学习的一个专栏 面向&#xff1a;广大C友 工具&#xff1a;VSCODE 博主&#xff1a;一个友好且宠粉的博主&#xff0c;送书活动小专栏&#xff0c;不定期抽奖送图书给粉丝 社区&#xff1a;&#x1f988;山鱼社区 1.如何配置C语言…

油电同价、标配8155,奇瑞猛攻10-15万中型SUV市场

8月8日&#xff0c;奇瑞瑞虎8冠军家族在北京国家奥林匹克体育中心正式上市&#xff0c;推出了瑞虎8 PRO冠军版、瑞虎8新能源冠军版两款新车&#xff0c;燃油混动双线同步发力。 其中&#xff0c;瑞虎8 PRO冠军版共推7款车型&#xff0c;官方指导价12.69万元-16.39万元&#xf…

cad中数据输入方法【直角坐标法,极坐标法,动态数据】

在 AutoCAD 中&#xff0c;点的坐标可以用直角坐标、极坐标、球面坐标和柱面坐标表示&#xff0c;每一种坐标又分别具有两种坐标输入方式&#xff1a;绝对坐标和相对坐标。其中直角坐标和极坐标最为 常用。 1.直角坐标法&#xff1a; 用点的 X、Y 坐标值表示的坐标。在命令行…

【Windows系统编程】03.远线程注入ShellCode

shellcode&#xff1a;本质上也是一段普通的代码&#xff0c;只不过特殊的编程手法&#xff0c;可以在任意环境下&#xff0c;不依赖于原有的依赖库执行。 远程线程 #include <iostream> #include <windows.h> #include <TlHelp32.h>int main(){HANDLE hPr…