基於dubbo框架下的RPC通訊協議性能測試

一、前言

  Dubbo RPC服務框架支持豐富的傳輸協議、序列化方式等通訊相關的配置和擴展。dubbo執行一次RPC請求的過程大致如下:消費者(Consumer)向註冊中心(Registry)執行RPC請求,註冊中心分配服務URL並路由到具體服務提供方(Provider),消費者和服務提供方建立網絡連接,服務提供方在本地創建連接池對象並提供遠程服務,對於長連接類型協議(如dubbo協議)將保持連接,減少握手認證,調用過程中可以避免頻繁建立和斷開連接導致的性能開銷,保持長連接需要有心跳包的發送,所以對於非頻繁調用的服務保持連接同樣會有消耗。

 dubbo共支持如下幾種通信協議:

  • dubbo://

  • rmi://

  • hessian://

  • http://

  • webservice://

  • thrift://

  • memcached://

  • redis://

部分協議的特點和使用場景如下:
  • 1、dubbo協議


    • Dubbo缺省協議採用單一長連接和NIO異步通訊,適合於小數據量大併發的服務調用,以及服務消費者機器數遠大於服務提供者機器數的情況。

  • 缺省協議,使用基於mina1.1.7+hessian3.2.1的tbremoting交互。 
    連接個數:單連接 
    連接方式:長連接 
    傳輸協議:TCP 
    傳輸方式:NIO異步傳輸 
    序列化:Hessian二進制序列化 
    適用範圍:傳入傳出參數數據包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供者,儘量不要用dubbo協議傳輸大文件或超大字符串。 
    適用場景:常規遠程服務方法調用

  • 爲什麼要消費者比提供者個數多: 
    因dubbo協議採用單一長連接, 
    假設網絡爲千兆網卡(1024Mbit=128MByte), 
    根據測試經驗數據每條連接最多隻能壓滿7MByte(不同的環境可能不一樣,供參考), 
    理論上1個服務提供者需要20個服務消費者才能壓滿網卡。

  • 爲什麼不能傳大包: 
    因dubbo協議採用單一長連接, 
    如果每次請求的數據包大小爲500KByte,假設網絡爲千兆網卡(1024Mbit=128MByte),每條連接最大7MByte(不同的環境可能不一樣,供參考), 
    單個服務提供者的TPS(每秒處理事務數)最大爲:128MByte / 500KByte = 262。 
    單個消費者調用單個服務提供者的TPS(每秒處理事務數)最大爲:7MByte / 500KByte = 14。 
    如果能接受,可以考慮使用,否則網絡將成爲瓶頸。

  • 爲什麼採用異步單一長連接: 
    因爲服務的現狀大都是服務提供者少,通常只有幾臺機器, 
    而服務的消費者多,可能整個網站都在訪問該服務, 
    比如Morgan的提供者只有6臺提供者,卻有上百臺消費者,每天有1.5億次調用, 
    如果採用常規的hessian服務,服務提供者很容易就被壓跨, 
    通過單一連接,保證單一消費者不會壓死提供者, 
    長連接,減少連接握手驗證等, 
    並使用異步IO,複用線程池,防止C10K問題。

  • 2、RMI

  • RMI協議採用JDK標準的java.rmi.*實現,採用阻塞式短連接和JDK標準序列化方式

  • Java標準的遠程調用協議。

  •        連接個數:多連接 
           連接方式:短連接 
           傳輸協議:TCP 
           傳輸方式:同步傳輸 
           序列化:Java標準二進制序列化 
           適用範圍:傳入傳出參數數據包大小混合,消費者與提供者個數差不多,可傳文件。 
           適用場景:常規遠程服務方法調用,與原生RMI服務互操作

  • 3、hessian

  • Hessian協議用於集成Hessian的服務,Hessian底層採用Http通訊,採用Servlet暴露服務,Dubbo缺省內嵌Jetty作爲服務器實現

  • 基於Hessian的遠程調用協議。

  •        連接個數:多連接 
           連接方式:短連接 
           傳輸協議:HTTP 
           傳輸方式:同步傳輸 
           序列化:Hessian二進制序列化 
           適用範圍:傳入傳出參數數據包較大,提供者比消費者個數多,提供者壓力較大,可傳文件。 
           適用場景:頁面傳輸,文件傳輸,或與原生hessian服務互操作

  • 4、http

  • 採用Spring的HttpInvoker實現

  • 基於http表單的遠程調用協議。

  •        連接個數:多連接 
           連接方式:短連接 
           傳輸協議:HTTP 
           傳輸方式:同步傳輸 
           序列化:表單序列化(JSON) 
           適用範圍:傳入傳出參數數據包大小混合,提供者比消費者個數多,可用瀏覽器查看,可用表單或URL傳入參數,暫不支持傳文件。 
           適用場景:需同時給應用程序和瀏覽器JS使用的服務。

  • 5、webservice

  • 基於CXF的frontend-simple和transports-http實現 
    基於WebService的遠程調用協議。

  •        連接個數:多連接 
           連接方式:短連接 
           傳輸協議:HTTP 
           傳輸方式:同步傳輸 
           序列化:SOAP文本序列化 
           適用場景:系統集成,跨語言調用。

  • 6、thrif

  • Thrift 是 Facebook 捐給 Apache 的一個 RPC 框架,當前 dubbo 支持的 thrift 協議是對 thrift 原生協議的擴展,在原生協議的基礎上添加了一些額外的頭信息,比如service name,magic number等。

二、測試方案

  基於dubbo 2.5.3框架,使用zookeeper作爲dubbo服務註冊中心,分別以單線程和多線程的方式測試以下方案:


Protocol      Transporter      Serialization    Remark
A dubbo 協議 netty hessian2
B dubbo 協議 netty dubbo
C dubbo 協議 netty java
D RMI 協議 netty java
E RMI 協議 netty hessian2
F Hessian 協議 servlet hessian2 Hessian,基於tomcat容器     
G WebService 協議   servlet SOAP CXF,基於tomcat容器  

 

三、傳輸測試數據

1、單POJO對象,嵌套複雜集合類型

2、POJO集合,包含100個單POJO對象

3、1K字符串

4、100K字符串

5、1M字符串 

 

四、服務接口和實現

1、服務接口相關代碼: 

package ibusiness;  
  
import java.util.List;  
  
import model.*;  
  
public interface IBusinessOrder {   
    public String SendStr(String str);   
  
    public List<OrderInfo> LoadOrders(List<OrderInfo> orders);   
  
    public OrderInfo LoadOrder(OrderInfo order);  
}

copy


2、服務實現相關代碼,測試數據在服務器端不做任何處理原樣返回:


package business;  
  
import ibusiness.IBusinessOrder;  
  
import java.util.List;  
  
import model.*;  
  
public class BusinessOrder implements IBusinessOrder {  
    public String SendStr(String str) {  
        return str;  
    }  
  
    public List<OrderInfo> LoadOrders(List<OrderInfo> orders) {  
        return orders;  
    }  
  
    public OrderInfo LoadOrder(OrderInfo order) {  
        return order;  
    }  
}

 cop 

五、單線程測試

1、測試僅記錄rpc調用時間,測試數據的讀取組裝以及首次建立連接等相關耗時時間不作統計,循環執行100次取平均值。  

2、服務消費方測試代碼


import java.util.List;  
  
import org.springframework.context.ApplicationContext;  
import org.springframework.context.support.FileSystemXmlApplicationContext;  
  
import com.alibaba.dubbo.rpc.service.EchoService;  
import common.Common;  
  
import ibusiness.*;  
import model.*;  
  
public class Program {  
    public static void main(String[] args) throws Exception {  
  
        ApplicationContext ctx = new FileSystemXmlApplicationContext("src//applicationContext.xml");  
        IBusinessOrder orderBusiness = (IBusinessOrder) ctx.getBean("orderBusiness");  
  
//        EchoService echoService = (EchoService) orderBusiness;  
//        String status = echoService.$echo("OK").toString();  
//        if (!status.equals("OK")) {  
//            System.out.println("orderBusiness out of service!");  
//            return;  
//        } else {  
//            System.out.println("orderBusiness in service !");  
//        }  
  
        long startMili, endMili;  
        int loop = 100;  
  
        // 單個pojo  
        try {  
            OrderInfo order = Common.BuildOrder();  
            orderBusiness.LoadOrder(order); // 防止首次連接的開銷  
  
            startMili = System.currentTimeMillis();  
            OrderInfo returnOrder = null;  
            for (int i = 0; i < loop; i++) {  
                returnOrder = orderBusiness.LoadOrder(order);  
            }  
            endMili = System.currentTimeMillis();  
            System.out.println("單個pojo 平均傳輸耗時爲:" + ((endMili - startMili) / (float) loop) + "毫秒 ,返回對象BillNumber:" + returnOrder.getBillNumber());  
        } catch (Exception ex) {  
            System.out.println("單個pojo 測試失敗!");  
            //ex.printStackTrace();  
        }  
  
        // pojo集合 (100)  
        try {  
            List<OrderInfo> orderList = Common.BuildOrderList();  
            startMili = System.currentTimeMillis();  
            List<OrderInfo> returnOrderList = null;  
            for (int i = 0; i < loop; i++) {  
                returnOrderList = orderBusiness.LoadOrders(orderList);  
            }  
            endMili = System.currentTimeMillis();  
            System.out.println("pojo集合 (100) 平均傳輸耗時爲:" + ((endMili - startMili) / (float) loop) + "毫秒 ,返回記錄數:" + returnOrderList.size());  
        } catch (Exception ex) {  
            System.out.println("pojo集合 (100) 測試失敗!");  
        }  
  
        // 1K String  
        try {  
            String str1k = Common.Build1KString();  
            startMili = System.currentTimeMillis();  
            String returnStr1k = null;  
            for (int i = 0; i < loop; i++) {  
                returnStr1k = orderBusiness.SendStr(str1k);  
            }  
            endMili = System.currentTimeMillis();  
            System.out.println("1K String 平均傳輸耗時爲:" + ((endMili - startMili) / (float) loop) + "毫秒,返回字符長度:" + returnStr1k.length());  
        } catch (Exception ex) {  
            System.out.println("1K String 測試失敗!");  
        }  
  
        // 100K String  
        try {  
            String str100K = Common.Build100KString();  
            startMili = System.currentTimeMillis();  
            String returnStr100k = null;  
            for (int i = 0; i < loop; i++) {  
                returnStr100k = orderBusiness.SendStr(str100K);  
            }  
            endMili = System.currentTimeMillis();  
            System.out.println("100K String 平均傳輸耗時爲:" + ((endMili - startMili) / (float) loop) + "毫秒,返回字符長度:" + returnStr100k.length());  
        } catch (Exception ex) {  
            System.out.println("100K String 測試失敗!");  
        }  
  
        // 1M String  
        try {  
            String str1M = Common.Build1MString();  
            startMili = System.currentTimeMillis();  
            String returnStr1M = null;  
            for (int i = 0; i < loop; i++) {  
                returnStr1M = orderBusiness.SendStr(str1M);  
            }  
            endMili = System.currentTimeMillis();  
            System.out.println("1M String 平均傳輸耗時爲:" + ((endMili - startMili) / (float) loop) + "毫秒,返回字符長度:" + returnStr1M.length());  
        } catch (Exception ex) {  
            System.out.println("1M String 測試失敗!");  
        }  
  
        System.out.println("all test done!");  
    }   
}




3、測試數據耗時記錄

A、dubbo 協議、netty 傳輸、hessian2 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="hessian2"  />

單個POJO0.958毫秒
POJO集合 (100)1.438毫秒
1K String0.68毫秒
100K String4.262毫秒
1M String32.473毫秒 

 

B、dubbo 協議、netty 傳輸、dubbo 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="dubbo" /> 

單個POJO1.45毫秒
POJO集合 (100)3.42毫秒
1K String0.94毫秒
100K String4.35毫秒
1M String27.92毫秒

 

C、dubbo 協議、netty 傳輸、java 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="java" /> 

單個POJO1.91毫秒
POJO集合 (100)4.48毫秒
1K String1.0毫秒
100K String3.3毫秒
1M String18.09毫秒

 

D、RMI 協議、netty 傳輸、java 序列化 

<dubbo:protocol name="rmi" server="netty" port="1099" serialization="java" />   

單個POJO1.63毫秒
POJO集合 (100)5.15毫秒
1K String0.77毫秒
100K String2.15毫秒
1M String15.21毫秒

 

E、RMI 協議、netty 傳輸、hessian2 序列化 

<dubbo:protocol name="rmi" server="netty" port="1099" serialization="hessian2"  /> 

單個POJO1.63毫秒
POJO集合 (100)5.12毫秒
1K String0.76毫秒
100K String2.13毫秒
1M String15.11毫秒

 

F、Hessian協議、servlet(tomcat容器)、hessian2 序列化 

<dubbo:protocol name="hessian" port="8080" server="servlet" serialization="hessian2" /> 

單個POJO1.6毫秒
POJO集合 (100)5.98毫秒
1K String1.88毫秒
100K String5.52毫秒
1M String39.87毫秒

 

G、WebService協議、servlet(tomcat容器)、SOAP序列化

<dubbo:protocol name="webservice" port="8080" server="servlet" /> 

單個POJO7.4毫秒
POJO集合 (100)34.39毫秒
1K String6.0毫秒
100K String7.43毫秒
1M String34.61毫秒

 

  4、性能對比

 

 

六、多線程測試

  1、由於測試機器配置較低,爲了避免達到CPU瓶頸,測試設定服務消費方Consumer併發10個線程,每個線程連續對遠程方法執行5次調用,服務提供方設置允許最大連接數100個,同時5個連接並行執行,超時時間設置爲5000ms,要求所有事務都能正確返回沒有異常,統計包含首次建立連接的消耗時間。

  2、服務消費方測試代碼

 

  3、測試數據耗時記錄

A、dubbo 協議、netty 傳輸、hessian2 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="hessian2"  /> 

單個POJO1165毫秒
POJO集合 (100)1311毫秒
1K String1149毫秒
100K String1273毫秒
1M String2141毫秒

 

B、dubbo 協議、netty 傳輸、dubbo 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="dubbo" /> 

單個POJO1220毫秒
POJO集合 (100)1437毫秒
1K String1145毫秒
100K String1253毫秒
1M String2065毫秒

 

C、dubbo 協議、netty 傳輸、java 序列化

<dubbo:protocol name="dubbo" server="netty" port="30001" serialization="java" /> 

單個POJO1188毫秒
POJO集合 (100)1401毫秒
1K String1123毫秒
100K String1227毫秒
1M String1884毫秒

 

D、RMI 協議、netty 傳輸、java 序列化 

<dubbo:protocol name="rmi" server="netty" port="1099" serialization="java" />   

單個POJO1751毫秒
POJO集合 (100)1569毫秒
1K String1766毫秒
100K String1356毫秒
1M String1741毫秒

 

E、RMI 協議、netty 傳輸、hessian2 序列化 

<dubbo:protocol name="rmi" server="netty" port="1099" serialization="hessian2"  /> 

單個POJO1759毫秒
POJO集合 (100)1968毫秒
1K String1239毫秒
100K String1339毫秒
1M String1736毫秒

 

F、Hessian協議、servlet、hessian2 序列化 

<dubbo:protocol name="hessian" port="8080" server="servlet" serialization="hessian2" /> 

單個POJO1341毫秒
POJO集合 (100)2223毫秒
1K String1800毫秒
100K String1916毫秒
1M String2445毫秒

 

G、WebService協議、servlet、SOAP序列化

<dubbo:protocol name="webservice" port="8080" server="servlet" /> 

單個POJO1975毫秒
POJO集合 (100)2768毫秒
1K String1894毫秒
100K String2098毫秒
1M String2887毫秒

 

  4、性能對比

 

七、性能分析

  測試過程中儘管考慮了非常多的影響因素,但仍然有很多侷限性,包括連接數限制、併發量、線程池策略、Cache、IO、硬件性能瓶頸等等因素,而且各自的適用場景不同,測試結果僅供參考

  從單線程測試結果可以看出,dubbo協議採用NIO複用單一長連接更適合滿足高併發小數據量的rpc調用,而在大數據量下的傳輸性能並不好,建議使用rmi協議,多線程測試中dubbo協議對小數據量的rpc調用同樣保持優勢,在大數據量的傳輸中由於長連接的原因對比rmi協議傳輸耗時差距並不明顯,這點同樣驗證了上述觀點。關於數據的序列化方式選擇需要考慮序列化和反序列化的效率問題,傳輸內容的大小,以及格式的兼容性約束,其中hessian2作爲duobb協議下的默認序列化方式,推薦使用。

  如果有描述錯誤或者不當的地方歡迎指正。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章