Microservices in Golang - Part1
原作者:Ewan Valentine
原文链接:https://ewanvalentine.io/microservices-in-golang-part-1/
引言
《Go微服务》系列文章包括十个部分,这是系列的第一篇。本系列中将会用到protobuf
与gRPC
作为服务间的通讯协议。我花了很长时间找到了这个简洁明了的解决方案,所以我想将我所了解的有关微服务项目创建、测试以及微服务端到端部署的知识分享给刚接触这个领域的新手们。
在教程中,我们将介绍一些基本概念,术语,并以最原始的形式创建我们的第一个微服务。
在整个系列中,我们将会创建以下服务:
- consighments
- users
- authentication
- vessels
我们需要用到的技术栈包括:golang, mongodb, grpc, docker, Google Cloud, Kubernetes(容器编排), NATS(消息中间件), CircleCI, Terraform(实例操作工具,代替kubectl), go-micro.
UPDATE 2020/05/21
- 感谢 @lihao 指出 go mod 使用上的问题
源码仓库:
先决条件
1 | $ go get -u google.golang.org/grpc |
我们要做什么?
我们也许会构建一个你能想到最简单的微服务实例,一个码头集装箱管理平台!对于微服务来说,构建一个博客未免有点杀鸡用牛刀,所以我想用一个项目既能展现微服务的低耦合,又具有一定的复杂度。这听起来像是一个有趣的挑战!
接下来就是正文内容了。
什么是微服务?
在传统整体应用 (Monolith Application) 中,所有的功能结构都被写入到了一个单体应用中。有时它们会被按照类型来进行分组,比如controllers
, entities
, factories
等,其他时候,大型应用可能按照关注点或者按照功能来进行划分,所以我们可能会有一个 auth
包,一个 friends
包,以及一个 articles
包。它们各自都有自己的 factories
, services
, repositories
, models
等,但最终它们都将会被组织到同一个代码库中。
微服务的理念就是在第二种方法上继续延伸:我们依然按照关注点或者功能拆分,并且这些包各自都是一个可以独立运行的代码库。
为什么用微服务?
复杂度 - 将功能划分成微服务使你可以将整个项目划分成更小的代码块,正如早期unix开发哲学——“做好一件事”。整体应用的发展趋势使功能结合得愈加紧密,且使得关注的边界变得模糊。随着复杂度的提升,出现漏洞的风险越来越高,集成的难度也越来越大。
拓展性 - 在单体应用中,某些代码的使用频率可能远高于其他部分,当需要扩展的时候,我们只能将整个代码库全部进行水平扩展,所以假设当你的认证服务访问频率非常高的时候,你需要将整个代码库都复制到新的服务器上来应对认证访问的需求。
微服务的理念,这种松耦合的理念允许你分别扩展单个服务,这意味着更高效的水平扩展。这点在多核、多区域云计算中能够发挥相当好的效果。
Nginx在微服务方面有很多非常好的文章,please give this a read.
为什么用Golang?
尽管微服务支持几乎所有的语言,但毕竟微服务只是一种概念而非具体的框架或者工具。一些语言对微服务有着更好的支持,而Golang就是其中之一。
Golang是一个轻量化、快捷的语言,同时它对并发提供了很好的支持,非常适合运行在多台多核机器上,能够充分发挥它们的性能。
Go语言同时包含了强大的标准库,对web服务有着良好的支持。
最后,Go语言有个强力的微服务框架 - go-micro,用它!
protobuf gRPC
protobuf gRPC 简述
由于微服务被拆分为不同的代码库,一个很重要的问题——如何在这些微服务之间进行通信。在单体应用中通讯并不是一个问题,因为你可以在代码库中直接调用其他代码。然而微服务并不能这样做,所以需要这些相互独立的应用需要一个相互通信的方法,延迟越低越好。
当然可以使用传统的REST方式,诸如基于http的JSON、XML。但是这个方法存在一个问题:服务A将数据编码成JSON/XML,并将字符串发送给服务B,B再将其解码需要付出一定的代价,这在一定规模的系统中有潜在的开销问题。尽管我们在同浏览器交互时必须采用这种方法,但是服务之间的通信用有更好的方法。
那就是 gRPC,gRPC是Google发行的一个基于二进制的轻量通讯协议。它本身包含很多东西,让我们来对它进行一些剖析。gRPC使用二进制作为其核心编码方式,以使用JSON的RESTful为例,我们通过http协议以字符串的形式发送数据,字符串中包含大量元数据,用来描述其编码格式、长度、内容的格式以及其他重要信息。这就是一台服务器向一个传统浏览器客户端发送其所需数据的方式。对于两个服务之间的通信来说,我们并不需要这么多东西,所以我们可以直接用二进制,这更轻量。gRPC使用新的HTTP 2.0规范,该规范允许使用二进制数据,它甚至允许双向流式传输,这非常酷!HTTP 2是gRPC运作的基础,关于HTTP 2.0,更多内容请看Google的 这篇文章.
但我们该怎么利用二进制数据呢?gRPC 有一个内置的数据交换协议名为 protobuf,Protobuf 允许我们使用对开发者更有好的格式定义一个微服务的接口。
那就让我们来定义我们第一个服务吧,在你仓库的根目录中创建 consignment-service/proto/consignment/consignment.proto
,在初期我会将我们所有的服务都放在一个仓库里面,是不是感觉还是整体仓库的结构?这是为了让教学更简单。关于是否使用单体仓库有很多争论,我们这里不去涉及,你当然也可以把服务和组件放在不同的仓库中。
译者注:
如果你能独立解决一些代码引用的问题,建议你还是将每个服务分别放到各自的仓库中,这样便于进行包的管理。
另外,其实在第四章中,作者还是把服务拆分到多个仓库了。
这里有一篇关于gRPC的文章我非常推荐。
定义 proto 文件
在刚才创建的 consignment.proto
中,加入一下内容:
1 | // consignment-service/proto/consignment/consignment.proto |
尽管这只是一个非常基础的例子,仍然有一些值得注意的地方。首先,你要定义你的service
,其中包含你希望暴露给其他服务的接口,其次你需要定义message
的类型,这其实就是你的数据结构。Protobuf是静态类型,你可以自定义类型,就像我们在Container
中定义的那样,message
就是我们定义的类型。
这里需要用的的库有两个,messages
由protobuf处理,我们定义的service
则由gRPC protobuf插件处理,该插件编译我们的定义,生成相应的代码,同诸如service
这些类型进行交互。
生成代码
protobuf的定义之后还需要通过命令行工具(CLI)生成支持二进制数据和函数的接口代码。在consignment-service/
路径下运行:
1 | protoc -I. --go_out=plugins=grpc:. \ |
这段命令会调用protoc库,负责将protobuf定义编译成代码。我们还指定使用grpc插件,同时构建了上下文和生成路径。
译者注:如果正使用go module,请先安装protobuf:
$ sudo apt install protobuf-compiler
protoc库
$ go get -u github.com/golang/protobuf/protoc-gen-go
运行这行命令,你可以在proto/consignment/
路径下发现新生成的代码,这个代码由gRPC/protobuf库自动生成,允许我们的代码使用protobuf定义生成的接口(interface)。
译者注:
可以参照 Makefile 文件,在路径下使用
make build
使操作更加简洁。在生成的
consignment.pb.go
中,可以看到Consignment
Container
Response
的实体,均对各个属性自动生成了相应的get方法。此外还有service
对应的实体ShippingServiceServer
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 // ShippingServiceServer is the server API for ShippingService service.
type ShippingServiceServer interface {
CreateConsignment(context.Context, *Consignment) (*Response, error)
}
// UnimplementedShippingServiceServer can be embedded to have forward compatible implementations.
type UnimplementedShippingServiceServer struct {
}
func (*UnimplementedShippingServiceServer) CreateConsignment(ctx context.Context, req *Consignment) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateConsignment not implemented")
}
func RegisterShippingServiceServer(s *grpc.Server, srv ShippingServiceServer) {
s.RegisterService(&_ShippingService_serviceDesc, srv)
}代码中定义了
ShippingServiceServer
的接口以及需要实现的方法CreateConsignment(context.Context, *Consignment)
,这个方法需要用户自行实现
实现接口
完成这些设置之后,创建 consignment-service/main.go
来实现这个接口:
1 | // consignment-service/main.go |
请仔细阅读代码中的注释。以上内容主要实现了 protobuf 中定义接口的方法,并在50051端口创建了一个 gRPC 服务器。接下来我们就有一个功能完整的gRPC服务了,你可以使用 go run main.go
运行,但是现在还无法直接调用这个服务,需要创建一个客户端(更准确一点,一个 CLI)来查看它的运行情况。
译者注:
repository
中我们定义了接口,在结构体Repository中我们实现了Create()
方法,之后结构体service
中调用了接口中的方法,而直到main()
中的这一步,我们才讲方法实现和接口进行绑定。(知道需要用的时候才bind)
1
2
3
4
5
6
7
8
9
10
11
12
13
14 func main() {
...
s := grpc.NewServer()
// Register our service with the gRPC server, this will tie our
// implementation into the auto-generated interface code for our
// protobuf definition.
// 这里通过&service{repo} 将实现了方法的&Repository{}传入了service
// 将方法与对象进行了绑定
pb.RegisterShippingServiceServer(s, &service{repo})
...
}
接下来使用 go mod
来初始化你的项目:
注:go mod 需要 go version 1.11及以上
1 | $ go mod init github.com/<YourUserName>/shippy-service-consignment |
译者注:
为了使用自己的
consignment.pb.go
,你需要将仓库上传到github中使用本地依赖替代
consignment.pb.go
方便测试:方法一
main.go
中的包导入可以先改为本地路径:
1
2
3
4 import (
...
pb "shippy-service-consignment/proto/consignment"
)方法二(不推荐)
如果想利用
replace
使用本地包替换调用远程参考的包,需要在proto/consignment
下使用go mod init
再在
shippy-service-consignment/go.mod
中加入:
1 replace github.com/fusidic/go-microsvc/consignment/proto/consignment => /root/workspace/go/go-microsvc/consignment-service/proto/consignment/consignment.pb.go可以看到,这种方法非常不优雅,需要分别建立 go mod,不推荐使用
方法三
尽早拆分程序,微服务就要有微服务的样子。
接下来创建一个 CLI (Command Line Interface), 它会从 JSON 文件中读取 consignment (货运) 信息,并与 gRPC 服务器交互。
接下来在根目录创建文件夹 mkdir consignment-cli
,并在其中创建一个 cli.go
:
1 | // consignment-cli/main.go |
创建一个货运 JSON 文件:
1 | { |
现在如果你在 consignment-service
中使用 $ go run main.go
,并创建一个新的终端,在 consignment-cli
中运行另外一个 $ go run cli.go
,此时你可以看见一条消息:Created: true
。
我们该如何确认真的完成创建了呢?让我们更新一下我们的微服务,加入一个 GetConsignments
方法,如此一来我们就可以看见我们创建的 consignments 了。
首先我们需要更新我们的 proto 定义(已在注释中标明)
1 | // consignment-service/proto/consignment/consignment.proto |
接下来就需要创建 GetConsignments
方法了,我们同样也需要新建一个 GetRequest
,目前暂时还不包括任何内容。同时我们也需要在 response 消息中加入 consignments
字段,你可能会注意到此处类型前的关键词 repeated
,这是为了将该类型作为数组来处理。
之后需要用之前提到的命令重新构建proto定义,我可能会看到类似这样的错误:*service does not implement consignment.ShippingServiceServer (missing GetConsignments method)
.
这是由于我们gRPC中的方法是基于定义的接口构建的,而此处我们需要实现新的接口。
更新 consignment-service/main.go
文件:
1 | package main |
以上,我们已经实现了 GetConsignments
方法,更新 git 仓库、实现 proto 定义的接口之后,如果再运行 $ go run main.go
,应该可以正常工作了。
接下来更新我们的cli工具 consignment-cli/cli.go
,在 main()
末尾添加:
1 | func main() { |
在主函数的最下面,我们增加“Created: success“消息的输出,重新运行 $ go run cli.go
,创建 consignment之后,再调用 GetConsignments
你就可以看见创建的consignments的列表。
注:为简洁起见,有时我可能会编辑以前用…编写过的代码,以表示未对之前的代码进行任何更改,但添加或添加了其他行。
好了,到这你应该已经成功的创建了一个微服务并创建了一个客户端通过gRPC与之交互了。
下一章节我们将介绍集成框架 go-micro,这是一个基于gRPC创建的微服务框架,而且我们将会创建我们的第二个服务,并使用Docker来容器化我们的服务。
关于这篇文章,如果你发现了错误或者有反馈,please drop me an email.
教程的仓库:
觉得这系列文章对您有帮助,可以请原作者喝杯咖啡,Cheers! https://monzo.me/ewanvalentine.
或者通过 Patreon 来支持原作者。
译者仓库:
更多请看
Article and newsletters
https://www.nginx.com/blog/introduction-to-microservices/
https://martinfowler.com/articles/microservices.html
https://www.microservices.com/talks/
https://medium.facilelogin.com/ten-talks-on-microservices-you-cannot-miss-at-any-cost-7bbe5ab7f43f#.ui0748oat
https://microserviceweekly.com/
Books
https://www.amazon.co.uk/Building-Microservices-Sam-Newman/dp/1491950358
https://www.amazon.co.uk/Devops-Handbook-World-Class-Reliability-Organizations/dp/1942788002
https://www.amazon.co.uk/Phoenix-Project-DevOps-Helping-Business/dp/0988262509
Podcasts
https://softwareengineeringdaily.com/tag/microservices/
https://martinfowler.com/tags/podcast.html
https://www.infoq.com/microservices/podcasts/