大白話帶你梳理一下Dubbo的那些事兒

  • 2020 年 2 月 21 日
  • 筆記

首先聲明,本文並不是什麼代碼實戰類型的文章,適合於想對dubbo有更加全面認識的讀者閱讀,文章不會過於深奧,只是將一系列的知識點串通起來,幫助讀者溫故而知新。

RPC服務的介紹

相信有過一些分佈式開發經歷的讀者都有用過一些RPC框架,通過框架包裝好之後提供的API接口調用遠程服務,體驗感覺起來就和調用本地服務一樣輕鬆。這麼方便好用的技術框架,在實際的開發過程中是如何包裝的呢?

很早的時候,國外的工程師設計了一種能夠通過A計算機調用B計算機上邊應用程序的技術,這種技術不需要開發人員對於網絡通訊了解過多,並且調用其他機器上邊程序的時候和調用本地的程序一樣方便好用。

A機器發起請求去調用B機器程序的時候會被掛起,B機器接收到A機器發起的請求參數之後會做一定的參數轉換,最後將對應的程序結果返回給A,這就是最原始的RPC服務調用了。

RPC調用的優勢

簡單

不需要開發者對於網絡通信做過多的設置,例如我們在使用http協議進行遠程接口調用的時候,總是會需要編寫較多的http協議參數(header,context,Accept-Language,Accept-Encode等等),這些處理對於開發人員來說,實際上都並不是特別友好。但是RPC服務調用框架通常都將這類解析進行了對應的封裝,大大降低了開發人員的使用難度。

高效

在網絡傳輸方面,RPC更多是處於應用層和傳輸層之間。這裡我們需要先理清楚一個問題,網絡分層。RPC是處於會話層的部分,相比處於應用層的HTTP而言,RPC要比Rest服務調用更加輕便。

常見的遠程調用技術

rmi

利用java.rmi包實現,基於Java遠程方法協議(Java Remote Method Protocol) 和java的原生序列化

Hessian

是一個輕量級的remoting onhttp工具,使用簡單的方法提供了RMI的功能。基於HTTP協議,採用二進制編解碼。

protobuf-rpc-pro

是一個Java類庫,提供了基於 Google 的 Protocol Buffers 協議的遠程方法調用的框架。基於 Netty 底層的 NIO 技術。支持 TCP 重用/ keep-alive、SSL加密、RPC 調用取消操作、嵌入式日誌等功能。

Thrift

是一種可伸縮的跨語言服務的軟件框架。它擁有功能強大的代碼生成引擎,無縫地支持C + +,C#,Java,Python和PHP和Ruby。thrift允許你定義一個描述文件,描述數據類型和服務接口。依據該文件,編譯器方便地生成RPC客戶端和服務器通信代碼。

最初由facebook開發用做系統內部語言之間的RPC通信,2007年由facebook貢獻到apache基金 ,現在是apache下的opensource之一 。支持多種語言之間的RPC方式的通信:php語言client可以構造一個對象,調用相應的服務方法來調用java語言的服務,跨越語言的C/S RPC調用。底層通訊基於SOCKET。

Avro

出自Hadoop之父Doug Cutting, 在Thrift已經相當流行的情況下推出Avro的目標不僅是提供一套類似Thrift的通訊中間件,更是要建立一個新的,標準性的雲計算的數據交換和存儲的Protocol。支持HTTP,TCP兩種協議。

Dubbo

Dubbo是 阿里巴巴公司開源的一個高性能優秀的服務框架,使得應用可通過高性能的 RPC 實現服務的輸出和輸入功能,可以和 Spring框架無縫集成。

上邊我們說到了RPC的遠程調用發展歷史,那麼下邊我們一起來深入探討一下RPC的服務。

首先我們來看看OSI的網絡協議內容。

OSI的七層網絡模型

對於OSI的七層網絡模型我繪製了下邊的這麼一張圖:

下邊是我個人對於這七層協議的理解:

  • 應用層 主要是對於服務接口的格式多定義,例如提供一定的終端接口暴露給外部應用調用。
  • 表示層 處理一些數據傳輸的格式轉換,例如說編碼的統一,加密和解密處理。
  • 會話層 管理用戶的會話和對話,建立不同機器之間的會話連接。
  • 傳輸層 向網絡層提供可靠有序的數據包信息。
  • 網絡層 真正發送數據包信息的層面,提供流和擁塞控制,從而降低網絡的資源損耗。
  • 數據鏈路層 封裝對應的數據包,檢測和糾正數據包傳輸信息。
  • 物理層 通過網絡通訊設備發送數據

HTTP & RPC

HTTP主要是位於TCP/IP協議棧的應用層部分,首先需要構建三次握手的鏈接,接着才能進行數據信息的請求發送,最後進行四次揮手斷開鏈接。

RPC在請求的過程中跨越了傳輸層和應用層,這是因為它本身是依賴於Socket的原因。(再深入的原因我也不知道)。減少了上邊幾層的封裝,RPC的請求效率自然是要比HTTP高效很多。

那麼一個完整的RPC調用應該包含哪些部分呢?

通常我們將一個完整的RPC架構分為了以下幾個核心組件:

  • Server
  • Client
  • Server Stub
  • Client Stub

這四個模塊中我稍微說下stub吧。這個單詞翻譯過來稱之為存根。

Client Stub 就是將客戶端請求的參數,服務名稱,服務地址進行打包,統一發送給server方。

Server Stub 我用通俗易懂的語言來解釋就是服務端接收到Client發送的數據之後進行消息解包,調用本地方法。(看過netty拆包機制應該會對這塊比較了解)。

Dubbo的核心屬性

其實Dubbo配置裏面的核心內容就是 服務暴露,服務發現,服務治理

什麼是服務暴露,服務發現,服務治理?

下邊我們用一段xml的配置來進行講解:

<?xml version="1.0" encoding="UTF-8"?>  <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"         xmlns="http://www.springframework.org/schema/beans"         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd      http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">      <dubbo:application name="dubbo-invoker-provider">          <dubbo:parameter key="qos.port" value="22222"/>      </dubbo:application>      <dubbo:registry address="zookeeper://127.0.0.1:2181"/>      <dubbo:protocol name="dubbo" port="20880"/>      <bean id="userService" class="com.sise.user.service.UserServiceImpl" />      <dubbo:service interface="com.sise.user.service.UserService" ref="userService" />  </beans>  

在dubbo的配置文件裏面,通常我們所說的dubbo:service 可以理解為服務暴露,dubbo:refernce 為服務發現,mock是服務治理,timeout屬於服務治理的一種(性能調優).

假設dubbo裏面希望將一些公共的配置抽取出來,我們可以通過properties文件進行配置,dubbo在加載配置文件的優先順序如下:

  1. 優先會讀取JVM -D啟動參數後邊的內容
  2. 讀取xml配置文件
  3. 讀取properties配置文件內容

dubbo默認會讀取dubbo.properties配置文件的信息,例如下邊這種配置:

dubbo.application.name=dubbo-user-service  dubbo.registry.address=zookeeper://127.0.0.1:2181  

假設我們的dubbo配置文件不命名為dubbo.properties(假設命名為了my-dubbo.properties)的時候,可以在啟動參數的後邊加上這麼一段指令:

 -Ddubbo.properties.file=my-dubbo.properties  

那麼在應用程序啟動之後,對應的工程就會讀取指定的配置文件,這樣就可以將一些共用的dubbo配置給抽取了出來。

XML和配置類的映射

在工作中,我們通常都會通過配置xml的方式來設定一個服務端暴露的服務接口和消費端需要調用的服務信息,這些配置的xml實際上在dubbo的源碼中都會被解析為對應的實體類對象。

例如說我們常用到的reference配置類,下邊我貼出一段代碼:

package com.sise.user.config;  import com.sise.user.service.UserService;  import com.sise.user.service.UserServiceImpl;  import org.apache.dubbo.config.*;  import java.io.IOException;  import java.util.concurrent.CountDownLatch;  /**   * dubbo裏面的自定義配置類   *   * @author idea   * @data 2019/12/29   */  public class DubboSelfDefConfig {      /**       * dubbo的服務暴露       */      public void server() {          ApplicationConfig applicationConfig = new ApplicationConfig();          applicationConfig.setName("dubbo-server-config");          RegistryConfig registryConfig = new RegistryConfig();          registryConfig.setAddress("zookeeper://127.0.0.1:2181");          ProtocolConfig protocolConfig = new ProtocolConfig();          protocolConfig.setName("dubbo");          protocolConfig.setPort(20880);          protocolConfig.setThreads(200);          UserService userService = new UserServiceImpl();          ServiceConfig<UserService> serviceConfig = new ServiceConfig<>();          serviceConfig.setApplication(applicationConfig);          serviceConfig.setRegistry(registryConfig);          serviceConfig.setProtocol(protocolConfig);          serviceConfig.setInterface(UserService.class);          serviceConfig.setRef(userService);          serviceConfig.export();      }        public void consumer() {          ApplicationConfig applicationConfig = new ApplicationConfig();          applicationConfig.setName("dubbo-client-config");          RegistryConfig registryConfig = new RegistryConfig();          registryConfig.setAddress("zookeeper://127.0.0.1:2181");          ReferenceConfig<UserService> referenceConfig = new ReferenceConfig<>();          referenceConfig.setApplication(applicationConfig);          referenceConfig.setRegistry(registryConfig);          referenceConfig.setInterface(UserService.class);          UserService localRef = referenceConfig.get();          localRef.echo("idea");      }      public static void main(String[] args) throws InterruptedException, IOException {          DubboSelfDefConfig d = new DubboSelfDefConfig();          d.consumer();          CountDownLatch countDownLatch = new CountDownLatch(1);          countDownLatch.await();      }  }  

在這段代碼裏面,通過案例可以發現有這些信息內容:

    UserService localRef = referenceConfig.get();      localRef.echo("idea");  

這兩行語句是獲取具體服務的核心之處,由於我在別處定義了一個叫做UserService 的公共服務接口,因此在服務引用的過程中可以進行轉換。

Dubbo2.7的三大新特新

Dubbo的github官方地址為 https://github.com/apache/dubbo

在這裡插入圖片描述

Dubbo 目前有如圖所示的 5 個分支,其中 2.7.1-release 只是一個臨時分支,忽略不計,對其他 4 個分支而言,我歸納了一下,分別有如下信息:

  • 2.5.x 近期已經通過投票,Dubbo 社區即將停止對其的維護。
  • 2.6.x 為長期支持的版本,也是 Dubbo 貢獻給 Apache 之前的版本,其包名前綴為:com.alibaba,JDK 版本對應 1.6。
  • 3.x-dev 是前瞻性的版本,對 Dubbo 進行一些高級特性的補充,如支持 rx 特性。
  • master 為長期支持的版本,版本號為 2.7.x,也是 Dubbo 貢獻給 Apache 的開發版本,其包名前綴為:org.apache,JDK 版本對應 1.8。

Dubbo 2.7 新特性

Dubbo 2.7.x 作為 Apache 的孵化版本,除了代碼優化之外,還新增了許多重磅的新特性,本文將會介紹其中最典型的2個新特性:

  • 異步化改造
  • 三大中心改造

異步化改造

1.異步化調用的方式,在Dubbo2.7版本裏面提供了異步化調用的功能,相關案例代碼如下所示:

@RestController  @RequestMapping(value = "/test")  public class TestController {      @Reference(async = true)      private UserService userService;        @GetMapping("/testStr")      public String testStr(String param){          return userService.testEcho(param);      }  }  

但是通過這種異步發送的方式我們通常都是獲取不到響應值的,所以這裡的return為null。

如果在低於2.7版本的dubbo框架中希望獲取到異步返回的響應值還是需要通過RPC上下文來提取信息。

代碼案例如下所示:

 @GetMapping("/futureGet")      public String futureGet(String param) throws ExecutionException, InterruptedException {          userService.testEcho(param);          Future<String> future= RpcContext.getContext().getFuture();          String result = future.get();          System.out.println("this is :"+result);          return result;      }  

通過RPC上下文的方式可以取到對應的響應值,但是這種方式需要有所等待,因此此時的效率會有所降低。假設我們將dubbo的版本提升到了2.7.1之後,通過使用CompletableFuture來進行接口優化的話,這部分的代碼實現就會有所變化:

/**   * @author idea   * @date 2019/12/31   * @Version V1.0   */  public interface DemoService {        String sayHello(String name) ;        default CompletableFuture<String> sayAsyncHello(String name){              return CompletableFuture.completedFuture(sayHello(name));        }  }  

調用方代碼:

package com.sise.consumer.controller;    import com.sise.dubbo.service.DemoService;  import org.apache.dubbo.config.annotation.Reference;  import org.springframework.web.bind.annotation.RequestMapping;  import org.springframework.web.bind.annotation.RestController;  import java.util.concurrent.CompletableFuture;  import java.util.concurrent.atomic.AtomicReference;  /**   * @author idea   * @date 2019/12/31   * @Version V1.0   */  @RestController  @RequestMapping(value = "/demo")  public class DemoController {        @Reference      private DemoService demoService;        @RequestMapping(value = "/testDemo")      public String testDemo(String name){          System.out.println("【testDemo】 this is :"+name);          return demoService.sayHello(name);      }.        @RequestMapping(value = "/testAsyncDemo")      public String testAsyncDemo(String name){          System.out.println("【testAsyncDemo】 this is :"+name);          CompletableFuture<String> future = demoService.sayAsyncHello(name);          AtomicReference<String> result = null;          //通過一條callback線程來處理響應的數據信息          future.whenComplete((retValue,exception)->{             if(exception==null){                 System.out.println(retValue);                 result.set(retValue);             } else {                 exception.printStackTrace();             }          });          return "通過一條callback線程來處理響應的數據信息,所以這個時候獲取不到信息響應";      }  }  

這樣的調用是藉助了callback線程來幫我們處理原先的數據內容,關於dubbo裏面的異步化調用,我借用了官方的一張圖來進行展示:

我們上邊講解的眾多方法都只是針對於dubbo的客戶端異步化,並沒有講解關於服務端的異步化處理,這是因為結合dubbo的業務線程池模型來思考,服務端的異步化處理比較雞肋(因為dubbo內部服務端的線程池本身就是異步化調用的了)。

當然dubbo 2.6 裏面對於接口異步化調用的配置到了2.7版本依舊有效。

三大中心的改造

註冊中心

在dubbo2.7之前,dubbo主要還是由consumer,provider ,register組成,然而在2.7版本之後,dubbo的註冊中心被拆解為了三個中心,分別是原先的註冊中心元數據中心以及配置中心

元數據配置

在dubbo2.7版本中,將原先註冊在zk上邊的過多數據進行了註冊拆分,這樣能夠保證減少對於zk端的壓力。具體配置如下:

<dubbo:registry address=「zookeeper://127.0.0.1:2181」 simplified="true"/>  

簡化了相應配置之後,dubbo也只會上傳一些必要的服務治理數據了,簡化版本的服務數據只剩下下邊這些信息:

dubbo://30.5.120.185:20880/com.sise.TestService?  application=test-provider&  dubbo=2.0.2&  release=2.7.0&  timestamp=1554982201973  

對於其他的元數據信息將會被存儲到一些元數據中心裏面,例如說redis,nacos,zk等

元數據配置改造主要解決的問題是:推送量大 -> 存儲數據量大 -> 網絡傳輸量大 -> 延遲嚴重

配置中心

dubbo2.7開始支持多種分佈式配置中心的組件。例如說:zk,Spring Cloud Config, Apollo, Nacos,關於這部分的配置網上的資料也比較多,我就不在這裡細說了。