從2.7.0-2.7.5版本,Dubbo調用鏈路是如何提高30%性能的

前言

Dubbo 2.7.5版本發佈也有快兩個月了,從2.7.0到2.7.5,Dubbo在性能優化上也作了很多事情,據官方的壓測結果,在QPS層面,從2.7.0到2.7.5,Dubbo 單次RPC調用鏈路性能提高了 30%,本文就帶你們看一看Dubbo作了哪些改動來提高RPC 調用鏈路性能。java

服務元數據靜態化,減小鏈路計算

consumer端緩存ConsumerModel

咱們知道當一個服務啓動時,若是配置了服務引用相關的配置時,在consumer端會生成所引用服務的代理對象,有關服務引用過程的源碼解析能夠直接掃下方二維碼關注公衆號【加點代碼調調味】獲取。在生成服務的代理對象前會作一些校驗配置以及更新配置等工做,它們主要是在ReferenceConfig的#checkAndUpdateSubConfigs()方法中實現的。在2.7.5版本中,該方法被新增了一些加載服務元數據的邏輯,下面來看看下面這部分源碼:緩存

public void checkAndUpdateSubConfigs() {
    /** * 省略無關代碼 */
    // part1-start
    //init serivceMetadata
    serviceMetadata.setVersion(version);
    serviceMetadata.setGroup(group);
    serviceMetadata.setDefaultGroup(group);
    serviceMetadata.setServiceType(getActualInterface());
    serviceMetadata.setServiceInterfaceName(interfaceName);
    // TODO, uncomment this line once service key is unified
    serviceMetadata.setServiceKey(URL.buildKey(interfaceName, group, version));
    // part2-start
    ServiceRepository repository = ApplicationModel.getServiceRepository();
    ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass);
    repository.registerConsumer(
            serviceMetadata.getServiceKey(),
            serviceDescriptor,
            this,
            null,
            serviceMetadata);
    // part2-end
    /** * 省略無關代碼 */
}
複製代碼

provider端緩存ProviderModel

當服務啓動時,必然會通過doExportUrls方法,具體的服務暴露過程源碼分析能夠直接掃下方二維碼關注公衆號【加點代碼調調味】獲取。跟consumer端同樣,在新版本中添加了加載和計算元數據的邏輯。看下面源碼:性能優化

private void doExportUrls() {
    // part1-start
    ServiceRepository repository = ApplicationModel.getServiceRepository();
    ServiceDescriptor serviceDescriptor = repository.registerService(getInterfaceClass());
    repository.registerProvider(
            getUniqueServiceName(),
            ref,
            serviceDescriptor,
            this,
            serviceMetadata
    );
    // part1-end
    /** * 省略無關代碼 */
}
複製代碼

看到這裏仍是一頭霧水吧,不要放棄,接着往下看:網絡

能夠看到consumer端的part1部分只是賦值一些服務相關的元數據,而consumer端的part2部分以及provider端的part1部分則是引入了ServiceRepository,ServiceRepository是服務倉庫,它封裝了服務相關的元數據、consumer端的元數據模型ConsumerModel以及provider端的元數據模型ProviderModel,ConsumerModel和ProviderModel兩個模型,分別封裝了consumer端和provider端的配置,好比ConsumerModel就封裝了referenceConfig、methodConfig等。在part2部分最重要的就是實例化了一個ServiceRepository對象,而後將version、group、服務類型、服務接口名等元數據放入ServiceRepository對象中,保證在進程啓動階段服務元數據儘可能作一些計算,作一些元數據以及計算結果的緩存,(好比生成方法參數描述parameterDesc數據等),從而在RPC調用鏈路上須要用到相關元數據時能夠直接能直接拿到計算結果。在下面幾個地方用到了ServiceRepository中緩存的元數據計算結果。ide

  • 初始化RpcInvocation時:不管是consumer端的調用鏈路中仍是provider端的調用鏈路中,RpcInvocation一直都是整個調用鏈路內攜帶元數據的載體,舉個例子:能夠看源碼中RpcInvocation有一個方法initParameterDesc(),這其中是賦值parameterDesc、compatibleParamSignatures、returnTypes這三個屬性,可是這三個數據都是直接從ServiceRepository中直接獲取的,並非在初始化RpcInvocation時再計算這三個值。
  • 解碼時:當provider端對請求進行解碼時,會解析須要調用的方法簽名,包括方法的參數類型、返回類型,如今這些內容都已經作了緩存,因此無需再從新解析,只要直接從ServiceRepository中獲取便可,具體的源碼能夠看DecodeableRpcInvocation的#decode(Channel channel, InputStream input)方法。

減小調用過程當中的 URL 操做產生的內存分配

除了在consumer端以及provider端在各自的調用鏈路中提高性能外,減小調用過程當中的URL操做產生的內存分配動做也是一個優化的點。在2.7.x之前的版本,也就是尚未元數據中心的版本,統一模型URL攜帶的內容極其多,這會致使在網絡中數據的傳輸中數據包太大,影響響應時間。而在2.7.x版本縮減了URL中的相關內容,讓URL只關注服務定位相關的元數據,好比protocol、host、port等。下面來看看在最新的版本中是如何作到減小調用過程當中的 URL 操做產生的內存分配的。主要從如下幾個方面着手:源碼分析

  • 減小了URL對於方法級別元數據獲取操做的內存分配
  • 減小URL.getAddress的對象分配

減小了URL對於方法級別元數據獲取操做的內存分配

public class URL implements Serializable {
    /** * 省略無關代碼 */
    private final Map<String, String> parameters;
    private final Map<String, Map<String, String>> methodParameters;
    private volatile transient Map<String, Map<String, Number>> methodNumbers;
  
    public static Map<String, Map<String, String>> toMethodParameters(Map<String, String> parameters) {
        Map<String, Map<String, String>> methodParameters = new HashMap<>();
        if (parameters == null) {
            return methodParameters;
        }

        String methodsString = parameters.get(METHODS_KEY);
        if (StringUtils.isNotEmpty(methodsString)) {
            String[] methods = methodsString.split(",");
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                String key = entry.getKey();
                for (String method : methods) {
                    String methodPrefix = method + '.';
                    if (key.startsWith(methodPrefix)) {
                        String realKey = key.substring(methodPrefix.length());
                        URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);
                    }
                }
            }
        } else {
            for (Map.Entry<String, String> entry : parameters.entrySet()) {
                String key = entry.getKey();
                int methodSeparator = key.indexOf('.');
                if (methodSeparator > 0) {
                    String method = key.substring(0, methodSeparator);
                    String realKey = key.substring(methodSeparator + 1);
                    URL.putMethodParameter(method, realKey, entry.getValue(), methodParameters);
                }
            }
        }
        return methodParameters;
    }
    /** * 省略無關代碼 */
}

複製代碼

在url中parameters屬性攜帶着服務相關的一些元數據配置,好比group、timeout等配置,包括方法級別的配置,還有服務相關的methods、interface等元數據,下圖是我跑的一個demo,其中展現了parameters的一些內容:性能

在新版本中將方法相關的元數據經過一個map維護在url中,減小對字符串的操做,由於每次須要獲取方法的元數據時,都須要從parameters中獲取對應的值,好比獲取sayHello.timeout的值,而後還要根據「.」分割獲取該配置的key爲timeout,最終才能獲取到sayHello這個方法的timeout配置,如今方法級別的元數據直接維護在url中,就不須要每次都進行字符串操做,而且還添加了緩存methodNumbers,加快二次獲取的速度。優化

減小URL.getAddress的對象分配

舊版本代碼ui

public class URL implements Serializable {
    /** * 省略無關代碼 */
    private final String host;
    private final int port;
    public String getAddress(String host, int port) {
        return port <= 0 ? host : host + ':' + port;
    }
    /** * 省略無關代碼 */
}
複製代碼

新版本代碼this

public class URL implements Serializable {
    /** * 省略無關代碼 */
    private transient String address;
    private final String host;
    private final int port;
    private static String getAddress(String host, int port) {
        return port <= 0 ? host : host + ':' + port;
    }
  	
    public String getAddress() {
        if (address == null) {
            address = getAddress(host, port);
        }
        return address;
    }
    /** * 省略無關代碼 */

}
複製代碼

從源碼看,在URL中新增了address這個屬性,在獲取address時只作一次對象分配,而不須要像原來每次調用getAddress方法時都作一些字符串拼接,因爲拼接的host和port都是一個String類型的對象,因此在拼接的時候並不會被在編譯期間就優化,而是會建立一個StringBuilder對象來進行拼接,這樣每次獲取address就會帶來對象的內存分配的性能損耗。

除了對於URL.getAddress的對象分配的優化外,在2.7.5版本發佈後,還有幾個pull request也是針對對象分配的優化,原理和這個差很少,這裏就不一一列舉了。

送福利區域

掃描下方二維碼關注公衆號【加點代碼調調味】
點擊菜單欄獲取免費49篇的《Dubbo源碼解析》系列文章

加點代碼調調味

foot
相關文章
相關標籤/搜索