• 代码细节带来的极致体验,ShardingSphere 5.1.0 性能提升密钥


    前言

    在 Apache ShardingSphere 被广泛采用的同时,我们接到了部分用户的反馈,ShardingSphere 5.x 相比过去版本性能略有下降。在 5.0.0 正式版发布后,ShardingSphere 团队对 ShardingSphere 的内核、接入端等方面进行了大量性能优化。目前 5.1.0 正式版已发布,本文将介绍 ShardingSphere 5.1.0 在代码层面所进行的部分性能优化,并对 ShardingSphere-Proxy 进行 TPC-C 基准测试验证优化成果。

    e76cc7f470c99ef9776fab1733c42a51.png

    吴伟杰

    SphereEx 基础设施研发工程师,Apache ShardingSphere Committer。目前专注于 Apache ShardingSphere 及其子项目 ElasticJob 的研发。

    优化内容

    更正 Optional 的使用方式

    Java 8 引入的 java.util.Optional 能够让代码更加优雅,例如避免方法直接返回 null。其中 Optional 有两个比较常用的方法:

    1. public T orElse(T other) {
    2.     return value != null ? value : other;
    3. }
    4. public T orElseGet(Supplier other) {
    5.     return value != null ? value : other.get();
    6. }

    在 ShardingSphere 的类 org.apache.shardingsphere.infra.binder.segment.select.orderby.engine.OrderByContextEngine 中有这么一段使用了 Optional 的代码:

    1. Optional result = // 省略代码...
    2. return result.orElse(getDefaultOrderByContextWithoutOrderBy(groupByContext));

    以上这种使用 orElse 的写法,即使 result 的结果不为空,orElse 里面的方法也会被调用,尤其是 orElse 里面的方法涉及修改操作时,可能会发生意料之外的事情。涉及方法调用的情况下应调整为下面的写法:

    1. Optional result = // 省略代码...
    2. return result.orElseGet(() -> getDefaultOrderByContextWithoutOrderBy(groupByContext));

    使用 lambda 提供一个 Supplier 给 orElseGet,这样只有 result 为空的时候才会调用 orElseGet 里面的方法。

    相关 PR:https://github.com/apache/shardingsphere/pull/11459/files

    避免高频并发调用 Java 8 ConcurrentHashMap 的 computeIfAbsent

    java.util.concurrent.ConcurrentHashMap 是我们在并发场景下比较常用的一种 Map,相比对所有操作以 synchronized 修饰的 java.util.HashtableConcurrentHashMap 在保证线程安全的情况下提供了更好的性能。但在 Java 8 的实现中,ConcurrentHashMap 的 computeIfAbsent 在 key 存在的情况下,仍然会在 synchronized 代码块中获取 value,在对同一个 key 高频调用 computeIfAbsent 的情况下非常影响并发性能。

    参考:https://bugs.openjdk.java.net/browse/JDK-8161372

    这个问题在 Java 9 解决了,但为了在 Java 8 上也能保证并发性能,我们在 ShardingSphere 的代码中调整写法规避这一问题。

    以 ShardingSphere 的一个高频调用的类 org.apache.shardingsphere.infra.executor.sql.prepare.driver.DriverExecutionPrepareEngine 为例:

    1. // 省略部分代码...
    2.     private static final Map TYPE_TO_BUILDER_MAP = new ConcurrentHashMap<>(81);
    3.     // 省略部分代码...
    4.     public DriverExecutionPrepareEngine(final String type, final int maxConnectionsSizePerQuery, final ExecutorDriverManager executorDriverManager, 
    5.                                         final StorageResourceOption option, final Collection rules) {
    6.         super(maxConnectionsSizePerQuery, rules);
    7.         this.executorDriverManager = executorDriverManager;
    8.         this.option = option;
    9.         sqlExecutionUnitBuilder = TYPE_TO_BUILDER_MAP.computeIfAbsent(type
    10.                 key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class, key, new Properties()));
    11.     }

    以上代码传入 computeIfAbsent 的 type 只有 2 种,而且这段代码是大部分 SQL 执行的必经之路,也就是说会并发高频地对相同 key 调用 computeIfAbsent 方法,导致并发性能受限。我们采用如下方式规避这一问题:

    1. SQLExecutionUnitBuilder result;
    2. if (null == (result = TYPE_TO_BUILDER_MAP.get(type))) {
    3.     result = TYPE_TO_BUILDER_MAP.computeIfAbsent(type, key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder.class, key, new Properties()));
    4. }
    5. return result;

    相关 PR:https://github.com/apache/shardingsphere/pull/13275/files

    避免高频调用 java.util.Properties

    java.util.Properties 是 ShardingSphere 在配置方面比较常用的一个类,Properties 继承了 java.util.Hashtable,因此要避免在并发情况下高频调用 Properties 的方法。

    我们排查到 ShardingSphere 与数据分片算法有关的类 org.apache.shardingsphere.sharding.algorithm.sharding.inline.InlineShardingAlgorithm 中存在高频调用 getProperty 的逻辑,导致并发性能受限。我们的处理方式为:将涉及 Properties 方法调用的逻辑放在 InlineShardingAlgorithm 的 init 方法内完成,避免在分片算法计算逻辑的并发性能。

    相关 PR:https://github.com/apache/shardingsphere/pull/13282/files

    避免使用 Collections.synchronizedMap

    在排查 ShardingSphere 的 Monitor Blocked 过程中,发现在 org.apache.shardingsphere.infra.metadata.schema.model.TableMetaData 这个类中使用了 Collections.synchronizedMap 修饰会被高频读取的 Map,影响并发性能。经过分析,被修饰的 Map 只会在初始化阶段有修改操作,后续都是读取操作,我们直接移除 Collections.synchronizedMap 修饰方法即可。

    相关 PR: https://github.com/apache/shardingsphere/pull/13264/files

    字符串拼接代替不必要的 String.format

    在 ShardingSphere 的类 org.apache.shardingsphere.sql.parser.sql.common.constant.QuoteCharacter 有这么一段逻辑:

    1. public String wrap(final String value) {
    2.         return String.format("%s%s%s", startDelimiter, value, endDelimiter);
    3.     }

    显然上面的逻辑就是做一个字符串拼接,但使用 String.format 的方式相比直接字符串拼接的开销会更大。我们修改成以下方式:

    1. public String wrap(final String value) {
    2.         return startDelimiter + value + endDelimiter;
    3.     }

    我们用 JMH 做一个简单的测试,测试结果:

    1. # JMH version: 1.33
    2. # VM version: JDK 17.0.1, Java HotSpot(TM) 64-Bit Server VM, 17.0.1+12-LTS-39
    3. # Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
    4. # Warmup: 3 iterations, 5 s each
    5. # Measurement: 3 iterations, 5 s each
    6. # Timeout: 10 min per iteration
    7. # Threads: 16 threads, will synchronize iterations
    8. # Benchmark mode: Throughput, ops/time
    9. Benchmark                           Mode  Cnt          Score         Error  Units
    10. StringConcatBenchmark.benchFormat  thrpt    9   28490416.644 ± 1377409.528  ops/s
    11. StringConcatBenchmark.benchPlus    thrpt    9  163475708.153 ± 1748461.858  ops/s

    可以看出,使用 String.format 相比使用 + 拼接字符串的开销会更大,且自 Java 9 起优化了直接拼接字符串的性能。由此可见选择合适的字符串拼接方式的重要性。

    相关 PR:https://github.com/apache/shardingsphere/pull/11291/files

    使用 for-each 代替高频 stream

    ShadingSphere 5.x 代码中使用了较多的 java.util.stream.Stream

    在我们之前做的一次 BenchmarkSQL(TPC-C 测试的 Java 实现) 压测 ShardingSphere-JDBC + openGauss 的性能测试中,我们发现将压测过程中发现的所有高频 stream 替换为 for-each 后,ShardingSphere-JDBC 的性能提升明显。

    f1d179c26edf3a86411a09d96b293e34.png

    *注:ShardingSphere-JDBC 与 openGauss 分别在 2 台 128 核 aarch64 的机器上,使用毕昇 JDK 8。

    以上测试结果也可能和 aarch64 平台及 JDK 有关。不过 stream 本身存在一定开销,性能在不同场景下差异较大,对于高频调用且不确定 stream 能够优化性能的逻辑,我们考虑优先使用 for-each 循环。

    相关 PR:https://github.com/apache/shardingsphere/pull/13845/files

    避免不必要的逻辑(重复)调用

    避免不必要的逻辑重复调用有很多案例:

    hashCode 计算

    ShardingSphere 有个类 org.apache.shardingsphere.sharding.route.engine.condition.Column 实现了 equals 和 hashCode 方法:

    1. @RequiredArgsConstructor
    2. @Getter
    3. @ToString
    4. public final class Column {
    5.     private final String name;
    6.     private final String tableName;
    7.     @Override
    8.     public boolean equals(final Object obj) {...}
    9.     @Override
    10.     public int hashCode() {
    11.         return Objects.hashCode(name.toUpperCase(), tableName.toUpperCase()); 
    12.     } 
    13. }

    显而易见,上面这个类是不可变的,但是却在hashCode 方法的实现中每次都调用方法计算 hashCode。如果这个对象频繁在 Map 或者 Set 中存取,就会多出很多不必要的计算开销。

    调整后:

     
     
    1. @Getter
    2. @ToString
    3. public final class Column {
    4.     private final String name;
    5.     private final String tableName;
    6.     private final int hashCode;
    7.     public Column(final String name, final String tableName) {
    8.         this.name = name;
    9.         this.tableName = tableName;
    10.         hashCode = Objects.hash(name.toUpperCase(), tableName.toUpperCase());
    11.     }
    12.     @Override
    13.     public boolean equals(final Object obj) {...}
    14.     @Override
    15.     public int hashCode() {
    16.         return hashCode;
    17.     } 
    18. }

    相关 PR:https://github.com/apache/shardingsphere/pull/11760/files

    使用 lambda 代替反射调用方法

    在 ShardingSphere 源码中,有以下场景需要记录方法及参数调用,并在需要的时候对指定对象重放方法调用:

    1. 向 ShardingSphere-Proxy 发送 begin 等语句;

    2. 使用 ShardingSpherePreparedStatement 为指定位置的占位符设置参数。

    以如下代码为例,重构前,使用反射的方式记录方法调用及重放,反射调用方法本身存在一定的性能开销,且代码可读性欠佳:

     
     
    1. @Override
    2. public void begin() {
    3.     recordMethodInvocation(Connection.class, "setAutoCommit"new Class[]{boolean.class}, new Object[]{false});
    4. }

    重构后,避免了使用反射调用方法的开销:

     
     
    1. @Override
    2. public void begin() {
    3.     connection.getConnectionPostProcessors().add(target -> {
    4.         try {
    5.             target.setAutoCommit(false);
    6.         } catch (final SQLException ex) {
    7.             throw new RuntimeException(ex);
    8.         }
    9.     });
    10. }

    相关 PR:

    • https://github.com/apache/shardingsphere/pull/10466/files

    • https://github.com/apache/shardingsphere/pull/11415/files

    Netty Epoll 对 aarch64 的支持

    Netty 的 Epoll 实现自 4.1.50.Final 支持 aarch64 架构的 Linux 环境。在 aarch64 Linux 环境下,使用 Netty Epoll API 相比 Netty NIO API 能够提升性能。

    参考:https://stackoverflow.com/a/23465481/7913731

    5.1.0 与 5.0.0 ShardingSphere-Proxy TPC-C 性能测试对比

    我们使用 TPC-C 对 ShardingSphere-Proxy 进行基准测试,以验证性能优化的成果。由于更早期版本的 ShardingSphere-Proxy 对 PostgreSQL 的支持有限,无法进行 TPC-C 测试,因此使用 5.0.0 与 5.1.0 版本对比。

    为了突出 ShardingSphere-Proxy 本身的性能损耗,本次测试将使用数据分片(1 分片)的 ShardingSphere-Proxy 对比 PostgreSQL 14.2。

    测试按照官方文档中的《BenchmarkSQL 性能测试(https://shardingsphere.apache.org/document/current/cn/reference/test/performance-test/benchmarksql-test/)》进行,配置由 4 分片缩减为 1 分片。

    测试环境


    ShardingSphere-Proxy

    PostgreSQL

    BenchmarkSQL

    CPU

    2 * Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz

    2 * Intel(R) Xeon(R) Gold 6146 CPU @ 3.20GHz

    2 * Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz

    RAM

    3 * 32GB 2400MHz

    16 * 32GB 2666MHz

    3 * 32GB 2400MHz

    硬盘

    /

    AVAGO SCSI 240GB

    /

    网卡

    Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection

    Intel Corporation Ethernet Connection X722 for 10GbE SFP+

    Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection

    操作系统

    CentOS 7.9

    CentOS 7.9

    CentOS 7.9

    软件环境

    Java 17.0.1

    ShardingSphere-Proxy 5.0.0 / 5.1.0

    PostgreSQL 14.2

    BenchmarkSQL 5.0

    其他配置

    网卡队列绑核 0-1,24-25

    ShardingSphere-Proxy 绑核 2-23,26-47

    fsync=off

    full_page_writes=off

    shared_buffers=128GB


    测试参数

    BenchmarkSQL 参数:

    • warehouses=192 (数据量)

    • terminals=192 (并发数)

    • terminalWarehouseFixed=false

    • 运行时间 30 mins

    PostgreSQL JDBC 参数:

    • defaultRowFetchSize=50

    • reWriteBatchedInserts=true

    ShardingSphere-Proxy JVM 部分参数:

    • -Xmx16g

    • -Xms16g

    • -Xmn12g

    • -XX:AutoBoxCacheMax=4096

    • -XX:+UseNUMA

    • -XX:+DisableExplicitGC

    • -XX:LargePageSizeInBytes=128m

    • -XX:+SegmentedCodeCache

    • -XX:+AggressiveHeap

    测试结果


    tpmC

    相比直连损耗

    PostgreSQL 14.2

    413,821

    /

    ShardingSphere-Proxy 5.0.0

    237,079

    42.7%

    ShardingSphere-Proxy 5.1.0

    300,558

    27.4%

    19a787e27184b32bd31268d13630db0b.png

    在本文的环境与场景中所得到的结论:

    • 以 ShardingSphere-Proxy 5.0.0 + PostgreSQL 为基准,5.1.0 性能提升约 26.8%

    • 以直连 PostgreSQL 为基准,ShardingSphere-Proxy 5.1.0 相比 5.0.0 损耗减少了约 15%,由 42.7% 降低至 27.4%。

    由于代码细节优化遍布 ShardingSphere 各模块,以上测试结果并未覆盖所有优化点。

    如何看待性能问题

    可能不时会有人问,“ShardingSphere 性能怎么样?损耗多少?”

    在我看来,性能能够满足需求即可。性能是一个比较复杂的问题,受非常多的因素影响。在不同的环境、场景下,ShardingSphere 的性能损耗有可能不到 1%,也有可能高达 50%,我们无法在脱离环境和场景的情况下给出答案。此外,ShardingSphere 作为基础设施,其性能是研发过程中重点考虑的因素之一,ShardingSphere 社区中的团队、个人也会持续发挥工匠精神,不断地将 ShardingSphere 的性能推向极致。

  • 相关阅读:
    OB38R08T1读24C64程序
    centos8启动kafka及kafka相关命令汇总
    VM虚拟机克隆
    ESP32 之 ESP-IDF 教学(十七)——组件依赖
    js setTimeout()与面试题
    MySQL/Redis 常见面试题汇总
    VScode断点调试vue
    scrcpy用法大全
    直流有刷电机驱动基于STM32F302R8+X-NUCLEO-IHM07M1(一)
    MongoDB索引
  • 原文地址:https://blog.csdn.net/ShardingSphere/article/details/123391044