Golang的微型服务-第1部分-2020更新

2020-06-12 20:30:04

这是关于用Golang编写微服务的十部分系列文章。使用Protobuf和GRPC作为底层传输协议。为什么?因为我花了很长时间才弄明白这一点,并敲定了一个清晰而简洁的解决方案,我想与其他刚接触这一领域的人分享我在创建、测试和部署微服务方面学到的东西。

在本教程中,我们将只介绍一些基本概念、术语,并以最简单的形式创建我们的第一个微服务。

我们最终得到的堆栈将是:Golang、MongoDB、GRPC、docker、Google Cloud、Kubernetes、NATS、CircleCI、Terraform和Go-Micro。

我们将构建可能是您能想到的最通用的微服务示例,一个运输集装箱管理平台!博客感觉微服务的用例太简单了,我想要的是能够真正展示复杂性分离的东西。所以这感觉像是一个很好的挑战!

在传统的整体式应用程序中,一个组织的所有功能都被写入到一个应用程序中。有时,它们会按其类型(如控制器、实体、工厂等)进行分组。有时,可能在较大应用程序中,功能按关注点或功能分开。因此,您可能有一个auth包、一个Friends包和一个文章包。它们可以包含它们自己的工厂、服务、储存库、模型等集合。但是最终它们被组合在单个代码库中。

微服务的概念是将第二种方法稍微深入一点,并将这些关注点分离到它们自己的、独立的、可运行的代码库中。

复杂性-将功能拆分成微服务允许您将代码拆分成更小的块。这让人回想起Unix那句古老的格言:把一件事做好。有一种趋势,与巨石,让领域变得彼此紧密耦合,并使关注变得模糊。这会导致更高风险、更复杂的更新,潜在更多错误和更困难的集成。

规模-在一个整体中,代码的某些区域可能比其他区域使用得更频繁。对于整体,您只能扩展整个代码库。因此,如果您的身份验证服务不断受到攻击,您需要扩展整个代码库来处理您的身份验证服务的负载。

对于微服务,这种分离允许您单独扩展各个服务。这意味着更有效的水平缩放。它与具有多个核心和区域等的云计算配合得非常好。

Nginx写了一个关于微服务各种概念的奇妙系列,请仔细阅读。

几乎所有语言都支持微服务,毕竟,微服务是一个概念,而不是一个特定的框架或工具。也就是说,一些语言比其他语言更适合和/或更好地支持微服务。有一种语言得到了极大的支持,那就是歌兰语。

Golang非常轻便、速度非常快,并且对并发有很好的支持,这在跨多台机器和内核运行时是一个强大的功能。

最后,GO有一个非常棒的微服务框架,称为GO-Micro。我们将在本系列中使用它。

因为微服务被分成单独的代码库,所以微服务的一个重要问题是通信。在整体中,通信不是问题,因为您可以直接从代码库中的其他地方调用代码。然而,微服务不具备这种能力,因为它们生活在不同的地方。因此,您需要一种方式,使这些独立的服务能够以尽可能少的延迟相互通信。

在这里,您可以使用传统的REST,比如基于http的JSON或XML。然而,这种方法的问题在于,服务A必须将其数据编码成JSON/XML,通过网络向服务B发送一个大字符串,然后服务B必须将来自JSON的消息解码回代码。这在规模上有潜在的开销问题。虽然你被迫在网络浏览器上采用这种通信方式,但服务之间可以用它们想要的任何格式进行交流。

GRPC进来了。GRPC是Google推出的基于二进制的轻量级RPC通信协议。这句话太多了,所以让我们来仔细分析一下。GRPC使用二进制作为其核心数据格式。在我们的RESTful示例中,使用JSON,您将通过http发送一个字符串。字符串包含大量关于其编码格式、长度、内容格式和各种其他零碎内容的元数据。这是为了使服务器可以通知传统的基于浏览器的客户端期望什么。在两个服务之间通信时,我们并不真正需要所有这些。所以我们可以使用冷硬双星,它的重量要轻得多。GRPC使用新的HTTP2.0规范,该规范允许使用二进制数据。它甚至允许双向流,这是相当酷的!HTTP 2对GRPC的工作方式非常重要。有关HTTP2的更多信息,请看一看来自Google的这篇精彩的帖子。

但是,我们如何处理二进制数据呢?好的,GRPC有一个交换DSL,称为协议缓冲区(Protobuf)。Protobuf允许您使用开发人员友好的格式定义服务的接口。

创建一个新的根目录,我已将其命名为my shippy。cd进入您的新根目录,并从我们的repo的根目录创建以下文件夹和文件:shippy-service-consignment/proto/consignment/consignment.proto。

就目前而言,我将我们所有的服务都放在一个回购中。这被称为单一回购(mono-repo)。这主要是为了简化本教程的内容。有很多支持和反对使用单一回复的论据,我不会在这里赘述。您可以将所有这些服务和组件放在单独的repos中,这种方法也有很多很好的论据。

//shippy-service-consignment/proto/consignment/consignment.protosyntax=";Proto3";;Package Consignment;service ShippingService{RPC CreateConsignment(Consignment)Returns(Response){}}消息寄售{String id=1;String description=2;int32 Weight=3;重复容器容器=4;String tainer_id=5;}消息容器{String id=1;string Customer_id=2;String Origin=3;String user_id=4;}消息响应{bool Created=1。

这是一个非常基本的例子,但这里有一些事情要做。首先,您定义了您的服务,它应该包含您希望向其他服务公开的方法。然后定义您的消息类型,这些类型实际上就是您的数据结构。Protobuf是静态类型的,您可以定义自定义类型,就像我们对Container所做的那样。消息本身只是自定义类型。

这里有两个库在工作,消息由protocol buf处理,我们定义的服务由GRPC protocol buf插件处理,该插件编译代码与这些类型交互,即我们的proto文件的服务部分。

然后,通过CLI运行该协议缓冲区定义,以生成将此二进制数据与您的功能接口的代码。

您在主机路径中设置了GOROOT或GO二进制文件位置。

这将调用Protoc库,该库负责将您的Protobuf定义编译成代码。我们还指定了GRPC插件的使用,以及构建上下文和输出路径。

现在,当您在proto/consignment目录中运行此命令时,您应该会看到一些新生成的代码。这是由GRPC/Protobuf库自动生成的代码,允许您将您的Protobuf定义与您自己的代码接口。

那么,让我们现在就把它设置好。从shippy-service-signment项目根目录创建main.go文件$touch main.go。

//Shippy-Service-Consignment/main.gopackage mainimport(";上下文";";日志";";NET";";sync";//导入生成的协议代码PB";github.com/<;YourUserName>;/shippy-service-consignment/proto/consignment";";google.golang.org/grpc";";google.golang.org/grpc/反射";)const(port=";:50051";)类型存储库接口{Create(*pb.Consignment)(*pb.Consignment,Error)}//存储库-虚拟存储库,这模拟了某种类型的数据存储//的使用。我们稍后将用实际的实现替换它。type Repository struct{musync.RWMutex Consignments[]*pb.Consignment}//创建新的寄售函数(repo*Repository)create(寄售*pb.Consignment)(*pb.Consignment,error){repo.mu.Lock()update:=append(repo.consignments,consignment)repo.consignments=更新的repo.。您可以在生成的代码本身中检查接口//以获取确切的方法签名等//以便更好地了解。type service struct{repo pository}//CreateConsignment-我们只在我们的服务上创建了一个方法,//这是一个Create方法,它接受上下文和请求作为//参数,这些由GRPC server.func(s*service)CreateConsignment(CTX context.Context,req*pb.Consignment)(*pb.Consignment)(*pb.Context,req*pb.Consignment)(*pb.Consignment)(*pb.Context)(*pb.Consignment)(*pb.Consignment)(*pb.。err:=s.repo.Create(Req)if err!=nil{return nil,err}//返回与我们在//protocol buf定义中创建的`Response`消息匹配的消息。回车(&A)。pb.Response{Created:true,Consignment:Consignment},nil}func main(){repo:=&;Repository{}//设置我们的GRPC服务器。LIS,err:=net.Listen(";tcp";,port)if err!=nil{log.Fatalf(";无法侦听:%v";,err)}s:=grpc.NewServer()//向GRPC服务器注册我们的服务,这将把我们的//实现绑定到//Protobuf定义的自动生成的接口代码中。pb.RegisterShippingServiceServer(s,&;service{repo})//在GRPC服务器上注册反射服务。如果err:=s.Serve(Lis);err!=nil{log.Fatalf(";无法提供服务:%v";,则在端口";上运行),则refection.Register log.Println(";在端口:";上运行)。,错误)}}。

请仔细阅读代码中留下的注释。但总而言之,这里我们正在创建GRPC方法与之交互的实现逻辑,使用生成的格式在端口50051上创建一个新的GRPC服务器。这就对了!功能齐全的GRPC服务。您可以使用$GO Run Main.Go运行此程序,但您不会看到任何内容,而且您还不能使用它……。因此,让我们创建一个客户端来查看它的运行情况。

注意:在本系列中,我们将使用新的go mod命令来处理依赖关系,因此请确保您使用的是GO 1.11及更高版本!

现在似乎是使用$go mod初始化我们的项目并获取依赖项的好时机:

让我们创建一个命令行界面,它将接受一个JSON寄售文件并与我们的GRPC服务交互。

现在在我们的根目录中创建一个名为:Shippy-cli-Consignment的项目,并创建一个新的主文件:

//shippy/shippy-cli-signment/main.gopackage mainimport(";编码/json";";io/ioutil";";log";";os";";Context";PB";github.com/<;YourUserName>;/shippy/shippy-service-consignment/proto/consignment";";google.golang.org/grpc";)const(地址=";本地主机:50051";defaultFilename=";寄售.json";)func parseFile(文件字符串)(*pb.Consignment,error){var寄售*pb.寄售数据,err:=ioutil.ReadFile(File)if err!=nil{return nil,err}json.Unmarshal(data,&;寄售)返回寄售,errconn,err:=grpc.Dial(address,grpc.WithInsecure())if err!=nil{log.Fatalf(";未连接:%v";,err)}推迟连接。Close()client:=pb.NewShippingServiceClient(Conn)//联系服务器并打印其响应。file:=defaultFilename如果len(os.Args)>;1{file=os.Args[1]}寄售,err:=parseFile(File)如果err!=nil{log.Fatalf(";无法解析文件:%v";,err)}r,err:=client.CreateConsignment(context.Background(),寄售)if err!=Nil{log.Fatalf(";无法问候:%v";,err)}log.Printf(";创建时间:%t";,r.Created)}。

5A.。我们将快速更新Shippy-service-Consignment的go.mod文件,使其指向本地存储库,而不是尝试从远程回购中提取。您可能希望在生产中删除它,但应该消除一些混淆和本地运行的时间,这是新的Go mod API的一个非常有用的功能:

{";description";:";这是测试寄售";,";重量";:550,";容器";:[{";Customer_id";:";Customer 001";,";user_id";:";user001";,";Origin";:";曼彻斯特,英国";}],";VEVER_ID";:";VEVEL001";}。

现在,如果您在Shippy-Consignment-service中运行$go run main.go,然后在单独的终端窗格中运行$go run main.go。您应该会看到一条消息,上面写着Created:True。但是我们怎么才能真正检查它创造了什么呢?让我们用GetConsignments方法更新我们的服务,这样我们就可以查看我们创建的所有寄售。

//shippy-service-consignment/proto/consignment/consignment.protosyntax=";Proto3";;Package Consignment;service ShippingService{RPC CreateConsignment(Consignment)Returns(Response){}//创建新方法RPC GetConsignments(GetRequest)Returns(Response){}}Message Containment{String id=1;String description=2;int32 Weight=3;Repeated Container Containers=4;String VEVER_ID=5;}消息容器{String ID=1;String Customer_。}//创建了一个空的GET RequestMessage GetRequest{}消息响应{bool Created=1;寄售=2;//在我们的通用响应消息中添加了多元寄售重复寄售=3;}。

因此,在这里,我们已经在我们的服务上创建了一个名为GetConsignments的新方法,我们还创建了一个暂时不包含任何内容的新GetRequest。我们还在回复消息中添加了寄售字段。您会注意到,这里的类型在实际类型之前有重复的关键字。正如您可能已经猜到的,这仅仅意味着将该字段视为这些类型的数组。

现在使用前面提到的命令重新构建您的原型定义。现在,再次尝试运行您的服务,您应该会看到类似于以下内容的错误:*service do not implementate consignment.ShippingServiceServer(Missing GetConsignments Method)。

因为我们的GRPC方法的实现是基于匹配由protobuf库生成的接口的,所以我们需要确保我们的实现与我们的proto定义相匹配。

软件包mainimport(";上下文";日志";";NET";同步";PB";github.com/<;YourUsername>;/shippy-service-consignment/proto/consignment";";google.golang.org/grpc";)const(端口=";:50051";)类型存储库接口{create(*pb.Consignment)(*pb.Consignment,error)getall()[]*pb.Consignment}//存储库-虚拟存储库,这模拟了某种类型的数据存储库//的使用。我们';稍后我将用实际的实现替换它。type Repository struct{musync.RWMutex Consignments[]*pb.Consignment}//创建新的寄售函数(repo*Repository)create(寄售*pb.Consignment)(*pb.Consignment,error){repo.mu.Lock()update:=append(repo.consignments,consignment)repo.consignments=更新的repo.mu.Unlock(。nil}//getall signmentsfunc(repo*Repository)getall()[]*pb.Consignment{return repo.consignments}//服务应该实现所有方法来满足我们在协议中定义的服务。您可以在生成的代码本身中检查接口//以获取确切的方法签名等//以便更好地了解。type service struct{repo pository}//CreateConsignment-我们只在我们的服务上创建了一个方法,//这是一个Create方法,它接受上下文和请求作为//参数,这些由GRPC server.func(s*service)CreateConsignment(CTX context.Context,req*pb.Consignment)(*pb.Consignment)(*pb.Context,req*pb.Consignment)(*pb.Consignment)(*pb.Context)(*pb.Consignment)(*pb.Consignment)(*pb.。err:=s.repo.Create(Req)if err!=nil{return nil,err}//返回与我们在//protocol buf定义中创建的`Response`消息匹配的消息。回车(&A)。pb.Response{Created:true,Consignment:Consignment},nil}//GetConsignments-func(s*service)GetConsignments(CTX context.Context,req*pb.GetRequest)(*pb.Response,Error){Consignments:=s.repo.GetAll()return&;pb.Response{Consignments:Consignments},nil}func main(){resignments:consignments},nil}func main(){resignments:=s.repo.GetAll()return&;pb.Response{consignments:consignments},nil}func main(){recLIS,err:=net.Listen(";tcp";,port)如果err!=nil{log.Fatalf(";未能侦听:%v";,err)}s:=grpc.NewServer()//向GRPC服务器注册我们的服务,这将把我们的//实现绑定到//protocol buf定义的自动生成的接口代码中。pb.RegisterShippingServiceServer(s,&;service{repo})log.Println(";在端口:";,port上运行)如果err:=s.Serve(Lis);err!=nil{log.Fatalf(";服务失败:%v";,错误)}}。

在这里,我们包含了新的GetConsignments方法,更新了存储库和接口,并满足了PROTO定义生成的接口。如果您运行$go run main。再次运行,应该可以再次运行。

让我们更新CLI工具,以包括调用此方法并列出我们的寄售货物的功能:

Func Main(){.getall,err:=client.GetConsignments(context.Background(),&;pb.GetRequest{})if err!=nil{log.Fatalf(";无法列出发货:%v";,err)}for_,v:=range getAll.Consignments{log.Println(V)}}。

在Main函数的最底部,在我们注销";Created:Success消息的下面,追加上面的代码,然后重新运行$go run cli.go。这将创建一个寄售,然后在之后调用GetConsignments。运行该列表的次数越多,您应该会看到该列表越多。

注意:为简洁起见,我有时可能会编校以前用……编写的代码。表示没有对前面的代码进行任何更改,但添加或附加了额外的行。

至此,我们已经使用Protobuf和GRPC成功创建了一个微服务和一个与其交互的客户端。

本系列的下一部分将围绕集成MICRO展开,这是一个用于创建基于GRPC的微服务的强大框架。我们还将创建我们的第二个服务,即集装箱服务。说到容器,让人困惑的是,在本系列的下一部分中,我们还将研究在Docker容器中运行我们的服务。

任何关于这篇文章的错误、错误或反馈,或者任何您认为有帮助的东西,请给我发一封电子邮件。

如果您觉得本系列很有用,并且您使用了广告拦截程序(谁会责怪您)。请考虑抛给我几英镑,以补偿我的时间和精力。干杯!。https://monzo.me/ewanvalentine