写点什么

如何使用 gRPC、Ballerina 和 Go 开发高效的微服务

2020 年 10 月 01 日

如何使用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


2020 年 10 月 01 日 10:001914

评论

发布
暂无评论
发现更多内容

程序员的晚餐 | 5月9日 炖蹄髈

清远

程序员人生

向上管理第一项:路径P背后的目标B

kimmking

管理

Redis 命令执行过程(上)

程序员历小冰

redis 源码分析

回"疫"录(11):别让善良寒了心

小天同学

疫情 回忆录 现实纪录 纪实

Java小想法: JDK许可证

范学雷

Java 编程语言

如何快速对应用系统做一个360度画像诊断?

姜戈

Java 运维 多线程 网络 内存

Scrum精髓 - Scrum的核心到底是什么

Bob Jiang

Scrum 敏捷 Scrum精髓 敏捷精髓

Redis学习笔记(集合类型)

编程随想曲

redis

理解雾计算(Fog Computing)与边缘计算(Edge Computing)

老任物联网杂谈

雾计算 Fog Computing 边缘计算 Edge Computing

Android Studio NDK 编译 Bsdiff 库

码农亮哥

android-studio ndk bsdiff

数据库连接池的大小

Java收录阁

数据库

软件产品开发流程

Interstate5

软件开发 软件开发流程

做程序员有未来吗

这小胖猫

程序员 个人成长 职业规划 技术人

ROS与OpenAI结合使用教程(概览)

辣么大

DevOps怎样影响开发运维人员

脚动两轮男之漂流小王子

DevOps

数据挖掘|cross_val_score交叉验证使用

黄大路

Python 数据挖掘 学习 数据分析

关于查尔斯-斯特里克兰

黄大路

提升认知 小说 个人提升 认识自己

真实 Java 笔试题

旭霁

Java 面试

LeetCode 1232. Check If It Is a Straight Line

liu_liu

LeetCode

高仿瑞幸小程序 05 更正轮播组件的高度计算

曾伟@喵先森

小程序 微信小程序 前端

软件产品信息安全 - 数据分类

Interstate5

软件开发 信息安全 数据分类

来了来了,2020 首场 Meetup ,可!

Apache Flink

大数据 flink 流计算 实时计算 大数据处理

产品经理中必会SQL技能,相关内容研发可不予支持

韩超

MySQL sql 产品经理

模块化设计思想产品设计应用

燕陈华

产品设计 模块化流程 流程图

Spring整合MyBatis详细分析

Java收录阁

mybatis

软件产品的信息安全问题

Interstate5

软件开发 信息安全

如何打造个人品牌?

石云升

个人 品牌

小棉袄,最终却没有变成你的防弹衣

小天同学

个人成长 成长 感悟 母亲节 感恩

《你好架构师之 压榨硬件价值的利器容器(Docker)》

再见小飞侠

面试官浅析程序员面试过程中的二三事

joe

互联网 个人成长 方法 职场 文化

白话计算机网络通信过程

WB

计算机网络 转行程序员

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

如何使用gRPC、Ballerina和Go开发高效的微服务-InfoQ