乾貨|效能提升金鑰,由程式碼細節帶來的極致體驗

SphereEx發表於2022-03-11

前言

眾所周知,程式碼是專案的核心所在,一段小小的程式碼可能會影響到整個專案的體驗。一個專案從 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:

避免高頻併發呼叫 Java 8 ConcurrentHashMap 的 computeIfAbsent

java.util.concurrent.ConcurrentHashMap是我們在併發場景下比較常用的一種 Map,相比對所有操作以   synchronized  修飾的   java.util.HashtableConcurrentHashMap在保證執行緒安全的情況下提供了更好的效能。但在 Java 8 的實現中, ConcurrentHashMap    computeIfAbsent  在 key 存在的情況下,仍然會在   synchronized  程式碼塊中獲取 value,在對同一個 key 高頻呼叫   computeIfAbsent  的情況下非常影響併發效能。

參考:

這個問題在 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<>( 81);
     // 省略部分程式碼...
    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:

避免高頻呼叫 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:

避免使用 Collections.synchronizedMap

在排查 ShardingSphere 的 Monitor Blocked 過程中,發現在   org.apache.shardingsphere.infra.metadata.schema.model.TableMetaData這個類中使用了   Collections.synchronizedMap修飾會被高頻讀取的 Map,影響併發效能。經過分析,被修飾的 Map 只會在初始化階段有修改操作,後續都是讀取操作,我們直接移除   Collections.synchronizedMap修飾方法即可。

相關 PR:

字串拼接代替不必要的 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:

使用 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:

避免不必要的邏輯(重複)呼叫

避免不必要的邏輯重複呼叫有很多案例:

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:

使用 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:

Netty Epoll 對 aarch64 的支援

Netty 的 Epoll 實現自   4.1.50.Final  支援 aarch64 架構的 Linux 環境。在 aarch64 Linux 環境下,使用 Netty Epoll API 相比 Netty NIO API 能夠提升效能。

參考:

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部落格 ” ,連結:http://blog.itpub.net/70001955/viewspace-2869363/,如需轉載,請註明出處,否則將追究法律責任。

相關文章