從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 調用鏈路性能。

<center>

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

/<center>

<center>

consumer端緩存ConsumerModel

/<center>

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

<code>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

/**
* 省略無關代碼
*/
}/<code>

<center>

provider端緩存ProviderModel

/<center>

當服務啟動時,必然會經過doExportUrls方法,跟consumer端一樣,在新版本中添加了加載和計算元數據的邏輯。看下面源碼:

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

看到這裡還是一頭霧水吧,不要放棄,接著往下看:

可以看到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中緩存的元數據計算結果。

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

<center>

減少調用過程中的 URL 操作產生的內存分配

/<center>

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

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

<center>

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

/<center>
<code>public class URL implements Serializable {
/**
* 省略無關代碼
*/
private final Map<string> parameters;
private final Map<string>> methodParameters;
private volatile transient Map<string>> methodNumbers;

public static Map<string>> toMethodParameters(Map<string> parameters) {
Map<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> 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> 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;
}
/**
* 省略無關代碼
*/
}/<string>/<string>/<string>/<string>/<string>/<string>/<string>/<string>/<code>

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

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

在新版本中將方法相關的元數據通過一個map維護在url中,減少對字符串的操作,因為每次需要獲取方法的元數據時,都需要從parameters中獲取對應的值,比如獲取sayHello.timeout的值,然後還要根據“.”分割獲取該配置的key為timeout,最終才能獲取到sayHello這個方法的timeout配置,現在方法級別的元數據直接維護在url中,就不需要每次都進行字符串操作,並且還添加了緩存methodNumbers,加快二次獲取的速度。

<center>

減少URL.getAddress的對象分配

/<center>

舊版本代碼

<code>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;
}
/**
* 省略無關代碼
*/
}/<code>

新版本代碼

<code>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;
}
/**
* 省略無關代碼
*/

}/<code>

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

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


分享到:


相關文章: