跳转到主要内容
Chinese, Simplified

关键的买点

  • 基于交互和通信风格,我们可以将微服务分为两组:面向外部的微服务和内部的微服务
  • RESTful API是面向外部的微服务的事实上的通信技术(REST的普遍性和丰富的支持生态系统在其持续成功中发挥了重要作用)。
  • gRPC是一种相对较新的远程过程调用(RPC) API范例实现。它可以在内部微服务之间的所有同步通信中发挥重要作用
  • 在这里,我们通过使用一个真实的微服务用例来研究关键的gRPC概念、它们的使用以及使用gRPC作为服务间通信的好处。
  • gRPC受到许多主要编程语言的支持。我们将使用Ballerinalang(舞女编程语言)和Golang作为编程语言来讨论示例实现。

在现代微服务体系结构中,我们可以根据微服务的交互和通信将其分为两大类。第一组微服务充当面向外部的微服务,直接向使用者公开。它们主要是基于http的api,使用传统的基于文本的消息传递负载(JSON、XML等),这些负载针对外部开发人员进行了优化,并使用具象状态传输(Representational State Transfer, 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作为默认传输协议,这使得gRPC比HTTP/1.1上的REST更快、更健壮。

您可以通过使用协议缓冲区来定义gRPC服务契约,其中每个服务定义使用参数的数据结构和返回类型来指定预期输入和输出消息的方法数量。使用主要的编程语言提供的工具,服务器端框架和客户端代码(存根)可以使用定义服务契约的相同协议缓冲文件生成。

一个使用gRPC的实用微服务用例

图1:在线零售商店微服务体系结构的一部分

微服务体系结构的主要好处之一是通过使用最合适的编程语言构建不同的服务,而不是用一种语言构建所有内容。图1演示了在线零售商店微服务体系结构的一个部分,其中四个微服务在Ballerina 舞女编程语言(在本文的其余部分中称为Ballerina 舞女编程语言)和Golang中一起实现,以提供在线零售商店的一些功能。由于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 .proto)

Order microservice将获得购物项目和数量,并返回小计。在这里,我使用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) {
       // Implementation goes here.
 
       // You should return an 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服务被完美地映射到Ballerina的服务类型,gRPC rpc映射到Ballerina的资源功能,gRPC消息被映射到Ballerina记录类型。

我已经为Order微服务创建了一个单独的Ballerina项目,并使用生成的OrderService样板代码来实现gRPC一元服务。

一元阻塞

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 .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;
 
       //Iterating through streamed messages here
       error? e = clientStream.forEach(function(Order order) {
           totalBill += order.subTotal;           
       });
       //Once the client completes stream, a grpc:EOS error is returned to indicate it
       if (e is grpc:EOS) {
           FinalBill finalBill = {
               total:totalBill
           };
           //Sending the total bill to the client
           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"]);
           }
       }
       //If the client sends an error instead it can be handled here
       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—一个用于向购物车添加商品,另一个用于执行最终的签出。在向购物车添加商品时,通过对order微服务执行gRPC调用并将其存储在内存中,它将获得一个带有每个商品小计的临时订单。调用Checkout微服务将把所有内存中存储的临时订单以gRPC流的形式发送给Checkout微服务,并返回需要支付的总额。Ballerina使用内置流类型和客户端对象抽象来实现gRPC客户端流。参见图2,其中演示了Ballerina的客户端流是如何工作的

图2:Ballerina gRPC客户机流

CheckoutService客户端流的完整实现可以在Cart微服务结帐资源功能中找到。最后,在结账过程中,对Golang中实现的库存微服务进行gRPC调用,并通过扣除已售商品更新库存。

grpc-gateway

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) = {
           // Route to this method from POST requests to /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 .proto)

在这个场景中,相同的UpdateStock服务将通过使用REST API调用作为外部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 并遵循自述README.md指令。

结论

gRPC相对较新,但其快速增长的生态系统和社区肯定会对微服务的发展产生影响。由于gRPC是一种开放标准,所有主流编程语言都支持它,这使得它非常适合在多语言微服务环境中工作。作为一种常规做法,我们可以将gRPC用于内部微服务之间的所有同步通信,并且通过使用gRPC -gateway等新兴技术,我们还可以将其公开为rest风格的api。除了本文讨论的内容之外,gRPC特性(如截止日期、取消、通道和xDS支持)将为开发人员构建有效的微服务提供强大的功能和灵活性。

更多的资源

欲了解更多有关芭蕾舞女gRPC支援的资料,请浏览以下连结:

Golang拥有全面的gRPC支持,我们可以扩展这些微服务,通过使用gRPC拦截器、截止日期、取消和渠道等,来增强安全性、健壮性和弹性。请参阅grpc-go git repo,其中有许多关于这些概念的工作示例。

推荐视频:

关于作者

Lakmal Warusawithana是WSO2的高级开发总监。2005年,Lakmal共同创立了thinkCube,这是为电信运营商量身定制的下一代协同云计算产品的先驱。他监督整个工程过程,特别关注thinkCube解决方案的可扩展性和服务交付。在共同创建thinkCube之前,Lakmal在ITABS工作了4年,这是一家专门从事基于Linux的服务器部署的公司,提供了一个定制的、易于使用的服务器管理界面。

 

原文:https://www.infoq.com/articles/microservices-grpc-ballerina-go/

本文:http://jiagoushi.pro/node/1355

讨论:请加入知识星球【快速和低代码开发】或者小号【it_training】或者QQ群【11107767】

Article
知识星球
 
微信公众号
 
视频号