如何使用gRPC、Ballerina和Go開發高效的微服務

本文要點:

  • 我們可以基於交互和通信方式將微服務分爲兩類:外部微服務和內部微服務。
  • RESTful API是面向外部微服務的事實上的通信技術(REST的無處不在和豐富的生態系統對其取得持續的成功起着至關重要的作用)。
  • gRPC是遠程過程調用(RPC)API範式的一種新的實現,在內部微服務間的同步通信方面發揮重要作用。
  • 在這篇文章裏,我們將通過真實的微服務案例來研究gRPC的關鍵概念,瞭解將gRPC作爲服務間通信的好處及其用法。
  • 很多主流的編程語言都支持gRPC。我們將使用Ballerina和Go作爲編程語言來探討示例。

在現代微服務架構中,我們可以基於微服務的交互和通信方式將微服務分爲兩種。第一種是直接暴露給消費者的面向外部的微服務。它們主要是基於HTTP的API,這些API使用基於常規文本的消息負載(JSON、XML等,針對外部開發人員進行了優化),並使用REST作爲事實上的通信技術。

REST的無處不在和豐富的生態系統對這些面向外部的微服務取得成功起着至關重要的作用。OpenAPI提供了定義良好的規範,用於描述、生成、使用和可視化這些REST API。API管理系統可以很好地與這些API配合使用,並提供安全性、速率限制、緩存和實現業務需求。GraphQL可以替代基於HTTP的REST API,但不在本文討論範圍之內。

另一種是內部微服務,不與外部系統或外部開發人員通信。這些微服務之間相互交互,以便完成給定的任務。內部微服務使用同步或異步通信。在很多情況下,我們可以看到內部微服務使用同步的基於HTTP的REST API,但這並不是最好的技術。在本文中,我們將仔細探究如何利用二進制協議(例如gRPC)作爲服務間通信的優化通信協議。

gRPC是什麼?

gRPC是用於服務間通信的相對較新的一種遠程過程調用(RPC)API範式。與其他RPC一樣,它允許位於不同服務器上的應用程序之間相互調用方法,就好像它們是本地對象一樣。與Thrift和Avro等其他二進制協議一樣,gRPC使用接口描述語言(IDL)來定義服務契約。gRPC使用HTTP/2(最新的網絡傳輸協議)作爲默認傳輸協議,與基於HTTP/1.1的REST相比,gRPC更快、更強大。

你可以使用Protocol Buffers來定義gRPC服務契約,每個服務定義指定方法數量,這些方法包含了期望的輸入和輸出消息以及參數和返回類型的數據結構。使用主流編程語言提供的工具,基於Protocol Buffers文件生成服務器端框架和客戶端代碼(存根)。

一個實際的gRPC微服務案例

圖1:一個在線零售商店微服務架構圖片段

微服務架構的一個主要好處是可以使用最合適的編程語言來構建不同的服務,而不是隻使用一種語言來構建所有服務。圖1展示了在線零售商店微服務架構的一部分,其中使用Ballerina實現了四個微服務(在本文的其餘部分用Ballerina代稱),使用Go實現了其他一些功能。由於主流的編程語言都支持gRPC,因此當我們在定義服務契約時,可以指定一種合適的編程語言。

syntax="proto3";
 
package retail_shop;
 
service OrderService {
   rpc UpdateOrder(Item) returns (Order);
}  
message Item {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;
}

清單1:Order微服務的服務契約(order.proto)
Order微服務根據購物項目和數量返回小計金額。在這裏,我使用Ballerina gRPC工具分別生成gRPC服務端樣板代碼和客戶端代碼。

$ ballerina grpc --mode service --input proto/order.proto --output gen_code

生成的OrderService服務器端樣板代碼如下:

import ballerina/grpc;
listener grpc:Listener ep = new (9090);
 
service OrderService on ep {
   resource function UpdateOrder(grpc:Caller caller, Item value) {
       // 具體實現
       // 返回一個Order對象
   }
}
public type Order record {|
   string itemNumber = "";
   int totalQuantity = 0;
   float subTotal = 0.0;
|};
public type Item record {|
   string itemNumber = "";
   int quantity = 0;
|};      

清單2:生成的樣板代碼片段(OrderService_sample_service.bal
gRPC的rpc映射到Ballerina的service類型,gRPC的rpc映射到Ballerina的resource function,gRPC的message映射到Ballerina的record類型。

我爲Order微服務創建了一個單獨的Ballerina項目,並使用生成的OrderService樣板代碼來實現gRPC服務

一元阻塞(Unary Blocking)

OrderService被Cart微服務調用。我們可以使用下面的Ballerina命令來生成客戶端存根代碼

$ ballerina grpc --mode client --input proto/order.proto --output gen_code

生成的客戶端存根同時具有阻塞和非阻塞遠程方法。這個示例代碼演示了gRPC一元服務如何與gRPC阻塞客戶端交互。

public remote function UpdateOrder(Item req, grpc:Headers? headers = ()) returns ([Order, grpc:Headers]|grpc:Error) {
       var payload = check self.grpcClient->blockingExecute("retail_shop.OrderService/UpdateOrder", req, headers);
       grpc:Headers resHeaders = new;
       anydata result = ();
       [result, resHeaders] = payload;
       return [<Order>result, resHeaders];
   }
};

清單3:生成的阻塞模式遠程對象代碼片段
Ballerina的遠程方法抽象與gRPC客戶端存根完美契合,你可以看到UpdateOrder調用代碼是多麼乾淨和整潔。

Checkout微服務對所有從Cart微服務收到的臨時訂單進行聚合,並生成最終的賬單。在本例中,我們將以訂單消息流的形式發送所有臨時訂單。

syntax="proto3";
package retail_shop;
 
service CheckoutService {
   rpc Checkout(stream Order) returns (FinalBill) {}
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;
}
message FinalBill {
   float total = 1;
}

清單4:Checkout微服務的服務契約(checkout.proto
你可以使用ballerina grpc命令爲checkout.proto生成樣板代碼

$ ballerina grpc --mode service --input proto/checkout.proto --output gen_code

gRPC客戶端流

Cart微服務將消息流作爲流對象參數,可以使用循環來訪問,以便處理客戶端發送的每一個消息。請看下面的實現:

service CheckoutService on ep {
   resource function Checkout(grpc:Caller caller, stream<Order,error> clientStream) {
       float totalBill = 0;
       //在這裏訪問消息流
       error? e = clientStream.forEach(function(Order order) {
           totalBill += order.subTotal;           
       });
       //客戶端的消息流結束時會返回一個grpc:EOS錯誤
       if (e is grpc:EOS) {
           FinalBill finalBill = {
               total:totalBill
           };
           //將總賬單發送給客戶端
           grpc:Error? result = caller->send(finalBill);
           if (result is grpc:Error) {
               log:printError("Error occurred when sending the Finalbill: " + 
result.message() + " - " + <string>result.detail()["message"]);
           } else {
               log:printInfo ("Sending Final Bill Total: " + 
finalBill.total.toString());
           }
           result = caller->complete();
           if (result is grpc:Error) {
               log:printError("Error occurred when closing the connection: " + 
result.message() +" - " + <string>result.detail()["message"]);
           }
       }
       //如果客戶端發送的是一個錯誤,可以這麼處理
       else if (e is grpc:Error) {
           log:printError("An unexpected error occured: " + e.message() + " - " +
                                                   <string>e.detail()["message"]);
       }   
   }
}

清單5:CheckoutService實現代碼片段(CheckoutService_sample_service.bal
一旦客戶端流結束,將返回grpc:EOS錯誤,這個錯誤可用來確定何時使用調用者對象向客戶端發送最終響應消息(聚合總數)。

示例客戶端代碼客戶端存根可以使用以下命令生成:

$ ballerina grpc --mode client --input proto/checkout.proto --output gen_code

我們來看一下Cart微服務的實現。Cart微服務有兩個REST API——一個用於向購物車添加商品,另一個用於執行最終的結賬。在向購物車添加商品時,通過gRPC調用Order微服務,並將其保存在內存中,它將獲得一個帶有每個商品小計金額的臨時訂單。調用Checkout微服務將把所有保存在內存中的臨時訂單以gRPC流的形式發送給Checkout微服務,並返回需要支付的總額。Ballerina使用內置的Stream類型和Client Object抽象來實現gRPC客戶端流。圖2演示了Ballerina的客戶端流是如何工作的。

圖2:Ballerina gRPC客戶端流

CheckoutService客戶端流的完整實現可以在Cart微服務的checkout resource function中找到。最後,在結賬過程中,通過gRPC調用使用Go實現的Stock微服務,並扣除已售商品,更新庫存。

syntax="proto3";
package retail_shop;
option go_package = "../stock;gen";
import "google/api/annotations.proto";
 
service StockService {
   rpc UpdateStock(UpdateStockRequest) returns (Stock) {
       option (google.api.http) = {
           // 路由到/api/v1/stock
           put: "/api/v1/stock"
           body: "*"
       };
   }
}
message UpdateStockRequest {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Stock {
   string itemNumber = 1;
   int32 quantity = 2;
}

清單6:Stock微服務的服務契約(stock.proto
在這個場景中,UpdateStock服務作爲外部API(通過REST API來調用),也可以通過gRPC進行服務間調用。grpc-gateway是protoc的一個插件,它讀取gRPC服務定義並生成一個反向代理服務器,將RESTful JSON API轉換爲gRPC。

圖3:grpc-gateway

你可以藉助grpc-gateway同時提供gRPC和REST風格的API。

使用以下命令生成Golang gRPC存根

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc:. \
stock.proto

使用以下命令生成Golang grpc-gateway代碼

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
stock.proto

使用以下命令生成stock.swagger.json

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
-I$GOROOT/src \
--swagger_out=logtostderr=true:../stock/gen/. \
./stock.proto

運行示例代碼

拉取microservices-with-grpc代碼庫,並參看READEME.md。

結論

gRPC雖然還年輕,但其快速增長的生態系統和社區肯定會對微服務的發展產生影響。由於gRPC是一種開放標準,所有主流編程語言都支持它,這使得它非常適合被用在多語言微服務環境中。作爲一種常規做法,我們可以將gRPC用於內部微服務之間的同步通信。藉助grpc-gateway等新興技術,我們還可以將其包裝成REST風格的API。除了本文討論的內容之外,gRPC特性(如超時取消通道xDS支持)將爲開發人員構建高效的微服務提供強大的功能和靈活性。

更多資源

要了解更多有Ballerina gRPC的資料,請瀏覽以下鏈接:

一元阻塞

一元非阻塞

服務器端流

客戶端流

雙向流

Go提供了全面的gRPC支持,我們可以擴展這些微服務,通過使用gRPC攔截器、超時、取消和渠道等來增強安全性、健壯性和彈性。請參閱grpc-go代碼庫,其中有很多有關這些概念的示例。

推薦視頻:

Generating Unified APIs with Protocol Buffers and gRPC

Writing REST Services for the gRPC curious

Using gRPC for Long-lived and Streaming RPCs

作者介紹:

Lakmal Warusawithana是WSO2的高級開發總監。2005年,Lakmal與其他人共同創立了thinkCube,這是爲電信運營商量身定製的下一代協同雲計算產品的先驅。他監督整個工程過程,特別關注thinkCube解決方案的可擴展性和服務交付。在創立thinkCube之前,Lakmal在ITABS工作了4年,這是一家專門從事Linux服務器部署的公司,提供易於使用的定製服務器管理界面。

原文鏈接

Building Effective Microservices with gRPC, Ballerina, and Go

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