关键要点
- 根据交互和沟通方式,我们可以将微服务分为两类:面向外部的微服务和内部微服务。
- RESTful API 是面向外部的微服务事实上的通信技术(REST 的普遍性和丰富的支持生态系统在其持续成功中发挥着至关重要的作用)。
- gRPC 是远程过程调用 (RPC) API 范式的一个相对较新的实现。它可以在内部微服务之间的所有同步通信中发挥主要作用
- 在这里,我们通过使用真实世界的微服务用例来检查关键 gRPC 概念、它们的用法以及将 gRPC 作为服务间通信的好处。
- 许多主要的编程语言都支持 gRPC。我们将讨论使用 Ballerinalang 和 Golang 作为编程语言的示例实现。
在现代微服务架构中,我们可以根据微服务的交互和通信将微服务分为两大类。第一组微服务充当面向外部的微服务,直接暴露给消费者。它们主要是基于 HTTP 的 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 服务契约, 其中每个服务定义指定具有预期输入和输出消息的方法的数量以及参数和返回类型的数据结构。使用主要编程语言提供的工具,可以使用定义服务合同的相同协议缓冲区文件生成服务器端框架和客户端代码(存根)。
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.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
清单 1:Order 微服务的服务契约 ( order.proto )
Order 微服务将获取购物项目和数量并返回小计。这里我使用 Ballerina gRPC 工具分别生成 gRPC 服务样板代码和存根/客户端。
$ ballerina grpc --mode service --input proto/order.proto --output gen_code
- 1.
这将生成 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;
|};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
清单 2:生成的样板代码的代码片段 ( OrderService_sample_service.bal )
gRPC 服务完美地映射到 Ballerina 的 service 类型,gRPC rpc 映射到 Ballerina 的类型, resource function gRPC 消息映射到 Ballerina 的record 类型。
我为 Order 微服务创建了一个单独的 Ballerina 项目,并使用生成的 OrderService 样板代码来实现 gRPC 一元服务。
一元阻塞
OrderService 在 Cart 微服务中被调用。我们可以使用以下 Ballerina 命令来生成客户端存根 和客户端代码。
$ ballerina grpc --mode client --input proto/order.proto --output gen_code
- 1.
生成的客户端存根具有阻塞和非阻塞远程方法。此示例代码演示了 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];
}
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
清单 3: 为阻塞模式生成的远程对象代码片段
Ballerina 的远程方法抽象是一个非常适合的 gRPC 客户端存根,您可以看到 UpdateOrder 调用代码 非常干净整洁。
Checkout 微服务通过汇总从 Cart 微服务收到的所有临时订单来发出最终账单。在这种情况下,我们会将所有临时订单作为 stream Order 消息发送。
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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
清单 4:Checkout 微服务的服务合同 ( checkout.proto )
您可以使用该 ballerina grpc 命令为 checkout.proto 生成样板代码 。
$ ballerina grpc --mode service --input proto/checkout.proto --output gen_code
- 1.
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"]);
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
清单 5:( CheckoutService_sample_service.bal )的Service 示例实现的代码片段 CheckoutService
客户端流完成后,将返回 grpc:EOS 错误,该错误可用于确定何时使用调用者对象向客户端发送最终响应消息(总计)。
CheckoutService的示例客户端代码 和客户端存根 可以使用以下命令生成:
$ ballerina grpc --mode client --input proto/checkout.proto --output gen_code
- 1.
让我们看一下 Cart 微服务的实现。Cart 微服务有两个 REST API — 一个用于将商品添加到购物车,另一个用于进行最终结账。将商品添加到购物车时,它会通过对 Order 微服务进行 gRPC 调用并将其存储在内存中,从而获得带有每个商品小计的临时订单。调用 Checkout 微服务会将所有存储在内存中的临时订单作为 gRPC 流发送到 Checkout 微服务,并返回要支付的总金额。Ballerina 使用内置的 Stream 类型和客户端对象抽象来实现 gRPC 客户端流。请参见图 2,它说明了 Ballerina 的客户端流式传输是如何工作的。
图 2:Ballerina gRPC 客户端流式传输
CheckoutService 客户端流的完整实现可以在 Cart 微服务结账资源功能中找到。最后,在结账过程中,对 Golang 实现的 Stock 微服务进行 gRPC 调用,并通过扣除已售商品来更新库存。
grpc-网关
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;
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
清单 6:Stock 微服务 ( stock.proto )的服务合同
在这种情况下,将通过使用 REST API 调用作为面向外部的 API 来调用相同的 UpdateStock 服务,并通过使用 gRPC 调用作为服务间调用来调用。grpc-gateway 是 protoc 的插件,它读取 gRPC 服务定义并生成一个反向代理服务器,将 RESTful JSON API 转换为 gRPC。
图 3:grpc 网关
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
- 1.
- 2.
- 3.
- 4.
- 5.
以下命令生成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
- 1.
- 2.
- 3.
- 4.
- 5.
以下命令生成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
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
样品运行
克隆microservices-with-grpc git repo 并按照 README.md 说明进行操作。
结论
gRPC 相对较新,但其快速发展的生态系统和社区肯定会对微服务开发产生影响。由于 gRPC 是一个开放标准,所有主流编程语言都支持它,因此非常适合在多语言微服务环境中工作。作为一般实践,我们可以使用 gRPC 进行内部微服务之间的所有同步通信,也可以使用 grpc-gateway 等新兴技术将其公开为 REST 风格的 API。除了我们在本文中讨论的内容之外,诸如 Deadlines、 Cancellation、 Channels和 xDS 支持等 gRPC 功能 将为开发人员构建有效的微服务提供强大的功能和灵活性。