Dubbo學習筆記(一)基本概念與簡單使用

語言: CN / TW / HK

其實上週是打算寫Dubbo的,但是發現Dubbo需要一個註冊中心,因為也有學習Dubbo的計劃,所以將Zookeeper和Dubbo放在一起介紹。

是啥?

我記得上一次看Dubbo的官網,Dubbo將自己定義為一款RPC 框架,到Dubbo3就變成了:

Apache Dubbo 是一款微服務開發框架,它提供了 RPC通訊 與 微服務治理 兩大關鍵能力。這意味著,使用 Dubbo 開發的微服務,將具備相互之間的遠端發現與通訊能力, 同時利用 Dubbo 提供的豐富服務治理能力,可以實現諸如服務發現、負載均衡、流量排程等服務治理訴求。同時 Dubbo 是高度可擴充套件的,使用者幾乎可以在任意功能點去定製自己的實現,以改變框架的預設行為來滿足自己的業務需求。

這裡再討論一下什麼是RPC(這一點我在RPC學習筆記初遇篇(一) 討論的已經很完備了),不少介紹RPC的文章都會先從一個應用想要呼叫另一個應用的函式入手,但這不如維基百科直觀:

分散式計算中,遠端過程呼叫(英語:Remote Procedure Call,RPC)是一個計算機通訊協議。該協議允許運行於一臺計算機的程式呼叫另一個地址空間(通常為一個開放網路的一臺計算機)的子程式,而程式設計師就像呼叫本地程式一樣,無需額外地為這個互動作用程式設計(無需關注細節).

那為什麼都從函式上入手,這是一種抽象和封裝,兩個程序需要進行通訊,需要在TCP之上制定標準,也就是制定應用層的協議,可以選擇HTTP(跨語言),也可以基於TCP,自定義應用層的協議。我們可以在Dubbo3概念架構一節的協議印證我們的觀點:

Dubbo3 提供了 Triple(Dubbo3)、Dubbo2 協議,這是 Dubbo 框架的原生協議。除此之外,Dubbo3 也對眾多第三方協議進行了整合,並將它們納入 Dubbo 的程式設計與服務治理體系, 包括 gRPC、Thrift、JsonRPC、Hessian2、REST 等。以下重點介紹 Triple 與 Dubbo2 協議。

最終我們選擇了相容 gRPC ,以 HTTP2 作為傳輸層構建新的協議,也就是 Triple。

也就是我們可以認為HTTP協議是RPC的一種。至於微服務治理,這裡不再重複的進行的討論,參考我掘金的文章: 《寫給小白看的Spring Cloud入門教程》。那既然你說HTTP協議是RPC的一種,那Dubbo的意義又何在呢,我個人認為是對HTTP協議進行改造吧,HTTP 2.0之前都是文字形式的,採取二進位制位元組流在網路中傳輸更快,除此之外使用HTTP協議傳送資料,還需要自己動手將資料進行序列化,如果需要跨語言通訊,定義的規則就更多了,Dubbo幫我們做好了這一切,甚至做的更多。這也就是我們學習Dubbo的意義所在。

梳理一下,RPC是一個計算機通訊協議,那為什麼都構造成了函式呼叫這一形式,這是因為從抽象來說是最合理的,我們可以大致推斷演進一下:

  • 首先是兩個程序需要進行交換資訊, 選擇了TCP作為傳輸層的協議, 有的人選擇了HTTP協議,因為這更簡單一些, 當交換的資訊比較簡單,各個高階語言的Socket API 是可以滿足其需求的。
  • 如果我們期待這種交換的資訊要更復雜一點呢,如果說我們選擇TCP或HTTP作為應用間通訊的形式,那麼就有一些重複性的編碼工作,比如取值,序列化為物件,如果是TCP協議還要考慮拆包等等,這無疑加重了程式設計師們編碼的負擔,那麼能不能簡化這個過程呢,遮蔽掉網路程式設計中涉及的複雜細節,抽象出來一個簡單的模型呢,高階語言都內建有函式,那不如就從函式入手,讓程序間交換資訊就像是呼叫兩個應用一樣,這也就是很多RPC教程都從函式入手的原因,我覺得是由程序通訊的過程中,為了遮蔽掉網路程式設計的複雜細節,選擇從函式入手,這樣讓人容易理解一些,而不是一開始就是函式呼叫的形式。換句話說,多數程式設計師可能沒了解過Socket 程式設計中的拆包之類的概念,但是一定理解函式這個概念,這是一種封裝。

而Dubbo雖然在官網將自己宣告為是一款微服務開發框架,但是在實際應用場景中,Apache Dubbo 一般會作為後端系統間RPC呼叫的實現框架,我們可以將其類比為HTTP協議對應的諸多HTTP Client。Dubbo提供了多語言支援,目前只支援Java、Go、Erlang這三種語言,那麼我們自然提出一個問題,不同語言內建的資料型別、方法形式是不一樣的,那作為RPC的實現者,它是如何做到跨語言的。

當然是引入一箇中間層-IDL

為了和計算機進行通訊,我們引入了程式語言,程式語言就是一箇中間層,那麼為了讓不同的高階語言進行通訊,Dubbo引入了IDL,Dubbo中推薦使用IDL定義跨語言服務,那什麼是IDL,Dubbo官方並沒有解釋,於是我去了維基百科:

An interface description language or interface definition language ( IDL ), is a generic term for a language that lets a program or object written in one language communicate with another program written in an unknown language. IDLs describe an interface in a language-independent way, enabling communication between software components that do not share one language, for example, between those written in C++ and those written in Java.

介面定義語言或者介面描述語言,是一種兩種不同的語言進行通訊的一種語言,IDL以獨立於語言的任何形式描述介面,支援不同的高階語言進行通訊。例如C++寫的應用和Java寫的應用

IDLs are commonly used in remote procedure call software. In these cases the machines at either end of the link may be using different operating systems and computer languages. IDLs offer a bridge between the two different systems.

IDL 通常在應用RPC,在RPC中通訊的雙方,鏈路的兩端通常是不同的作業系統和程式語言。IDL為兩個不同的系統提供了橋樑。

為什麼又把英文貼出來了,維基百科不是也有中文嗎?下面是維基百科中IDL的中文解釋:

介面描述語言 (Interface description language,縮寫 IDL ),是用來描述軟體元件介面的一種計算機語言。IDL通過一種獨立於程式語言的方式來描述介面,使得在不同平臺上執行的物件和用不同語言編寫的程式可以相互通訊交流;比如,一個元件用C++寫成,另一個元件用寫成。

看到這個介面我懵了一下,我估計是對interface的翻譯,interface的中文有介面的意思。那既然是一種計算機語言,我們合情推理,那就有語法,在Dubbo中提供了IDL的示例:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.apache.dubbo.demo";
option java_outer_classname = "DemoServiceProto";
option objc_class_prefix = "DEMOSRV";

package demoservice;

// The demo service definition.
service DemoService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

Dubbo是如是描述的:

以上是使用 IDL 定義服務的一個簡單示例,我們可以把它命名為 DemoService.proto ,proto 檔案中定義了 RPC 服務名稱 DemoService 與方法簽名 SayHello (HelloRequest) returns (HelloReply) {} ,同時還定義了方法的入參結構體、出參結構體 HelloRequestHelloReply 。 IDL 格式的服務依賴 Protobuf 編譯器,用來生成可以被使用者呼叫的客戶端與服務端程式設計 API,Dubbo 在原生 Protobuf Compiler 的基礎上提供了適配多種語言的特有外掛,用於適配 Dubbo 框架特有的 API 與程式設計模型。

又出現了一個新名詞: Protobuf, Protobuf是啥?Apache Dubbo沒有解釋,我只好再訴諸於搜尋引擎:

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. --《Protocol Buffers》官網
Protocol Buffers 是Google 為序列化結構資料設計的一種獨立於語言、平臺的一種可擴充套件機制, 類似於XML, 但是更小、更簡單、更快。你只需要定義資料如何被結構化,然後用生成的原始碼,就能夠在不同的語言中讀取和寫入你的結構化資料。

XML是一種描述資料的一種形式,那既然是類似於XML,那又是一種描述資料的一種形式,結合上面的跨語言語境,也就是說我們藉助對應的Protobuf編譯器用proto來生成呼叫客戶端與服務端程式設計API。

Protobuf 簡單入門

既然是描述資料,那麼就會有資料型別,Protobuf為了跨語言,聲明瞭一些資料型別, 與各個語言的資料型別有對應的對映關係 , 這裡簡單列出一一下和java資料型別的對映關係:

  • double ==> java double
  • float ==> java float
  • int64 ==> java long
  • uint32 ==> java int
  • bool ==> java bool
  • String ==> java String

上面的示例中HelloRequest、HelloReply的欄位每個都進行了賦值,但這並不是預設值, 而是欄位編號,這些欄位編號用於標識二進位制形式的欄位。到目前為止我們就只剩上面的幾個optional看不懂了:

  • java_multiple_files

如果為true, 每個message 和 service 都會被生成為一個類。如果是false,則所有的message和service都會被生成到一個類中。

  • java_package

生產的程式碼所處的位置,如果沒有則會產生在package 後面宣告的包。

  • java_outer_classname

生產服務的名稱。

  • objc_class_prefix

很奇怪官方的示例為什麼會把這個放進去,我查了很多資料,這個語法為objective-c所提供,用於為指定的類生成字首。

Dubbo 還說:

使用 Dubbo3 IDL 定義的服務只允許一個入參與出參,這種形式的服務簽名有兩個優勢,一是對多語言實現更友好,二是可以保證服務的向後相容性,依賴於 Protobuf 序列化的相容性,我們可以很容易的調整傳輸的資料結構如增、刪欄位等,完全不用擔心介面的相容性

到現在為止我們已經看懂了官方的示例,現在我們就要用起來。

基本使用示例

Dubbo官方推薦使用IDL,那我們還是使用官方的示例,來定義服務。官方提供了示例:

我這裡貼下指令:

git clone -b master https://github.com/apache/dubbo-samples.git
cd dubbo-samples/dubbo-samples-protobuf
# 要求配置maven的環境變數
mvn clean package
# 執行 Provider
java -jar ./protobuf-provider/target/protobuf-provider-1.0-SNAPSHOT.jar 
# 執行 consumer
java -jar ./protobuf-consumer/target/protobuf-consumer-1.0-SNAPSHOT.jar

然後你會發現跑不起來, 我跑是這樣:

Zookeeper連線不上,這裡批評一下Apache Dubbo的官方示例文件,完全跑不起來,真的是在用心寫文件嗎! 這個Zookeeper我們在《Zookeeper學習筆記(一)基本概念和簡單使用》已經介紹過了,一個分散式協調服務,提供命名服務。那Dubbo這個示例中為什麼要求連線Zookeeper呢,為了解耦合,我們如果直接通過IP+埠的方式去調服務提供者的服務的話,這樣就耦合在一起了,假設生產上換臺機器我們還得改程式碼,再有就是叢集的情況下,我知道服務名就好,不需要知道特定ip的,這也就是註冊中心的概念,服務提供者將服務註冊到註冊中心,消費者提供服務名和腰消費的服務即可。Dubbo服務常見的架構:

Monitor是監控,監控服務呼叫,這裡我們不做介紹。其實在Dubbo提供的原始碼中也預設連線了Zookeeper這個註冊中心:

還好我們已經裝過了Zookeeper,我們將地址改掉就行。注意哈,高版本的JDK改動了很多東西,Dubbo 官方提供的示例,在JDK 17下可能跑不起來,如果到時候編譯報錯,將環境調整到JDK8就行,我自己測試的話,JDK 11也可以的,但是有的時候會報Zookeeper連線不上的錯誤。我用IDEA啟動一下:

然後啟動消費者:

發現是沒問題的,我分析了一下為啥在我的windows power shell 中出現Zookeeper 連線不上的原因可能是我配置的JDK環境變數是 JDK 11的,在IDEA中能夠成功跑起來的原因是IDEA用的是JDK8.

頗有種你發任你發,我接著用JDK8的感覺。

從示例中分析

pom裡面有Protobuf外掛:

<plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier}</protocArtifact>
                     <!--將protobuf檔案輸出到這個目錄-->
                    <outputDirectory>build/generated/source/proto/main/java</outputDirectory>
                    <clearOutputDirectory>false</clearOutputDirectory>
                    <protocPlugins>
                        <protocPlugin>
                            <id>dubbo</id>
                            <groupId>org.apache.dubbo</groupId>
                            <artifactId>dubbo-compiler</artifactId>
                            <version>${compiler.version}</version>
                            <mainClass>org.apache.dubbo.gen.dubbo.Dubbo3Generator</mainClass>
                        </protocPlugin>
                    </protocPlugins>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
  </plugin>
public class ConsumerApplication {
    public static void main(String[] args) throws Exception {
        // 載入Spring的上下文檔案
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-consumer.xml");
        context.start();
        // 從容器中獲取demoService
        DemoService demoService = context.getBean("demoService", DemoService.class);
        // 構建入參
        HelloRequest request = HelloRequest.newBuilder().setName("Hello").build();
        // 實現RPC
        HelloReply reply = demoService.sayHello(request);
        System.out.println("result: " + reply.getMessage());
        System.in.read();
    }
}
public class Application {
    public static void main(String[] args) throws Exception {
        // 載入bean
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/dubbo-provider.xml");
        context.start();
        System.out.println("dubbo service started");
        // 避免應用關閉
        new CountDownLatch(1).await();
    }
}
/**
 * 真正的實現類
 */
public class DemoServiceImpl implements DemoService {
    private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);

    @Override
    public HelloReply sayHello(HelloRequest request) {
        logger.info("Hello " + request.getName() + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return HelloReply.newBuilder()
                .setMessage("Hello " + request.getName() + ", response from provider: "
                        + RpcContext.getContext().getLocalAddress())
                .build();
    }

    @Override
    public CompletableFuture<HelloReply> sayHelloAsync(HelloRequest request) {
        return CompletableFuture.completedFuture(sayHello(request));
    }
}

總結一下

程序間的通訊可以直接使用應用層的協議如HTTP、也可以基於TCP自定義應用層的協議,但是對於面向物件的高階語言來說,資料接過來說還要有一個序列化過程,如果說是基於TCP的話,我們還要考慮拆包的問題,我們都喜歡的東西,我們能否遮蔽中間的通訊細節呢,兩個程序的通訊就像是呼叫各自的函式一樣,這也就是RPC,但是如果兩個程序是用不同的語言編寫的呢,為了語言中立,我們引入IDL,跨語言,但是通訊還是要選擇應用層的協議,要麼自己基於TCP,要麼基於已有的應用層協議,比如說HTTP,但是現在已經有高度成熟的RPC框架了,你不需要關心那麼多HTTP協議的通訊細節、以及序列過程,在一定的配置下,你可以實現像調本地函式一樣,調另一個程序的函式,高度的封裝。RPC是在演進的過程中選擇了函式作為載體,這是為了遮蔽掉通訊和序列化的細節,而不是一開始就是就是以函式的形式出現,本質上是一種通訊協議。

參考資料