ITPub博客

首页 > IT职业 > IT职场 > 干货|性能提升密钥,由代码细节带来的极致体验

干货|性能提升密钥,由代码细节带来的极致体验

原创 IT职场 作者:SphereEx 时间:2022-03-11 13:45:09 0 删除 编辑

前言

众所周知,代码是项目的核心所在,一段小小的代码可能会影响到整个项目的体验。一个项目从 0 到 1,从成长到成熟,离不开代码的精心打磨。细节决定成败,一个优秀的开源项目也正是如此,本篇干货经验贴,将以 ShardingSphere 5.1.0 性能提升为例,带大家感受代码细节带来的极致体验,如何在代码上实现飞跃。

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

优化内容

更正 Optional 的使用方式

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



public T 
orElse(
T other) {

     return  value !=  null ?  value : other;
}

public T  orElseGet( Supplier<? extends T> other) {
     return  value !=  null ?  value : other. get();
}

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

Optional<OrderByContext> result = 
// 省略代码...

return result.orElse(getDefaultOrderByContextWithoutOrderBy(groupByContext));

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

Optional<OrderByContext> result = 
// 省略代码...

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.Hashtable, ConcurrentHashMap在保证线程安全的情况下提供了更好的性能。但在 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为例:

 
// 省略部分代码...

    private  static  final  Map< String, SQLExecutionUnitBuilder> TYPE TOBUILDER MAP =  new ConcurrentHashMap<>( 8,  1);
     // 省略部分代码...
    public DriverExecutionPrepareEngine( final <span class="hljs-built
in" style="font-size: inherit; line-height: inherit; margin: 0px; padding: 0px; color: rgb(170, 87, 60); word-wrap: inherit !important; word-break: inherit !important;">String type,  final  int maxConnectionsSizePerQuery,  final ExecutorDriverManager<C, ?, ?> executorDriverManager, 
                                         final StorageResourceOption option,  final Collection<ShardingSphereRule> rules) {
         super(maxConnectionsSizePerQuery, rules);
         this.executorDriverManager = executorDriverManager;
         this.option = option;
        sqlExecutionUnitBuilder = TYPE TOBUILDER_MAP.computeIfAbsent(type, 
                key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder. class, key,  new Properties()));
    }

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

SQLExecutionUnitBuilder result;

if ( null == (result = TYPE_TO_BUILDER_MAP. get(type))) {
    result = TYPE_TO_BUILDER_MAP.computeIfAbsent(type, key -> TypedSPIRegistry.getRegisteredService(SQLExecutionUnitBuilder. class, key,  new Properties()));
}
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有这么一段逻辑:

 

public String 
wrap(
final String 
value) {

         return String.format( "%s%s%s", startDelimiter,  value, endDelimiter);
    }

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



public String 
wrap(
final String 
value) {

         return startDelimiter +  value + endDelimiter;
    }

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


# JMH version: 1.33

# VM version: JDK 17.0.1, Java HotSpot(TM) 64-Bit Server VM, 17.0.1+12-LTS-39
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 3 iterations, 5 s each
# Measurement: 3 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 16 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
Benchmark                           Mode  Cnt          Score         Error  Units
StringConcatBenchmark.benchFormat  thrpt     9    28490416. 644 ±  1377409. 528  ops/s
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 的性能提升明显。

* 注: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方法:


@RequiredArgsConstructor

@Getter
@ToString
public  final  class  Column {

     private  final String name;

     private  final String tableName;

     @Override
     public  boolean  equals ( final Object obj) {...}

     @Override
     public  int  hashCode () {
         return Objects.hashCode(name.toUpperCase(), tableName.toUpperCase()); 
    } 
}

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

调整后:


@Getter

@ToString
public  final  class  Column {

     private  final String name;

     private  final String tableName;

     private  final  int hashCode;

     public  Column ( final String name,  final String tableName) {
         this.name = name;
         this.tableName = tableName;
        hashCode = Objects.hash(name.toUpperCase(), tableName.toUpperCase());
    }

     @Override
     public  boolean  equals ( final Object obj) {...}

     @Override
     public  int  hashCode () {
         return hashCode;
    } 
}

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

使用 lambda 代替反射调用方法

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

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

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

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


@Override

public  void  begin () {
    recordMethodInvocation(Connection.class,  "setAutoCommit",  new Class[]{ boolean.class},  new Object[]{ false});
}

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


@Override

public  void  begin () {
    connection.getConnectionPostProcessors().add(target -> {
         try {
            target.setAutoCommit( false);
        }  catch ( final SQLException ex) {
             throw  new RuntimeException(ex);
        }
    });
}

相关 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 分片。

测试环境

测试参数

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

测试结果

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

  • 以 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 的性能推向极致。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70001955/viewspace-2869363/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
欢迎来我的专属社区找我交流呀~ https://community.sphere-ex.com

注册时间:2021-06-10

  • 博文量
    126
  • 访问量
    60568