在Golang和gRPC上构建微服务架构,第2部分(docker)

是时候应对集装箱了


首先,我们使用最新的Linux Alpine映像。 Linux Alpine是一个轻量级Linux发行版,其设计和优化可以在Docker中运行Web应用程序。 换句话说,Linux Alpine具有足够的依赖性和功能来运行大多数应用程序。 这意味着图像大小约为8 MB!

相比之下,例如容量约为1 GB的Ubuntu虚拟机,这就是Docker映像对于微服务和云计算变得更加自然的原因。

因此,现在我希望您看到容器化的价值,我们可以开始“ Dockerizing”我们的第一个服务。 让我们创建一个Dockerfile $ touch consignment -service / Dockerfile



第一部分
原始的EwanValentine存储库
原始文章

在Dockerfile中,添加以下内容:

FROM alpine:latest RUN mkdir /app WORKDIR /app ADD consignment-service /app/consignment-service CMD ["./consignment-service"] 

然后,我们创建一个新目录来承载我们的应用程序。 然后,将已编译的二进制文件添加到Docker容器中并运行它。

现在,让我们更新Makefile的构建记录以创建Docker映像。

 build: ... GOOS=linux GOARCH=amd64 go build docker build -t consignment . 

我们又添加了两个步骤,我想更详细地解释它们。 首先,我们创建Go二进制文件。 但是,在运行$ go build之前,您会注意到两个环境变量。 GOOS和GOARCH允许您交叉编译另一个操作系统的二进制文件。 由于我是为Macbook开发的,因此无法编译go可执行文件,然后在使用Linux的Docker容器中运行它。 二进制文件在您的Docker容器中将完全没有意义,并且将引发错误。

我添加的第二步是docker构建过程。 Docker将读取您的Dockerfile并创建一个名为consignment-service的映像,点表示目录路径,因此在这里我们只希望构建过程查看当前目录。

我将向我们的Makefile添加一个新条目:

 run: docker run -p 50051:50051 shippy-service-consignment 

在这里,我们通过打开端口50051启动Docker映像。由于Docker在单独的网络层上运行,因此您需要重定向端口。 例如,如果要在端口8080上启动此服务,则必须将-p参数更改为8080:50051。 您还可以通过包含-d标志在后台运行容器。 例如, docker run -d -p 50051:50051 consignment-service

运行$ make run ,然后再次在单独的终端面板中运行$ go run main.go并检查它是否仍然有效。

运行$ docker build时,您将代码和运行时嵌入到映像中。 Docker映像是您的环境及其依赖项的可移植映像。 您可以通过将Docker映像发布到Docker Hub来共享它们。 这类似于npm或docker映像的yum存储库。 在Dockerfile中定义FROM时,您告诉Docker从Docker存储库中提取此映像以用作基础。 然后,您可以展开和重新定义此基本文件的各个部分,并根据需要重新定义它们。 我们不会发布docker映像,但是可以随意浏览docker存储库,并注意到几乎所有软件都已经打包在容器中。 一些非常美妙的事情被码头化了。

Dockerfile中的每个广告在首次构建时都会被缓存。 这样就无需在每次更改时都重建整个运行时。 码头工人足够聪明,可以找出哪些细节已更改以及需要重建哪些细节。 这使得构建过程非常快。

足够的容器! 让我们回到我们的代码。

创建gRPC服务时,有许多用于创建连接的标准代码,您需要对服务地址在客户端或其他服务中的位置进行硬编码,以便它可以与其连接。 这很困难,因为在云中启动服务时,它们可能不会使用同一主机,或者在重新部署服务后地址或ip可能会更改。

这是发现服务发挥作用的地方。 发现服务将更新所有服务的目录及其位置。 每个服务都在运行时注册,并在关闭时注销。 然后为每个服务分配一个名称或标识符。 因此,即使服务名称保持不变,即使它可能具有新的IP地址或主机地址,也不需要从其他服务更新对此服务的调用。

通常,有很多方法可以解决此问题,但是,就像编程中的大多数事情一样,如果有人已经解决了这个问题,那么重新发明轮子是没有意义的。 Go-micro的创建者@Chuhnk(Asim Aslam)以出色的清晰度和易用性解决了这些问题。 他单枪匹马制作出了不起的软件。 如果您喜欢自己所看到的,请考虑帮助他!

微型化


Go-micro是一个用Go编写的功能强大的微服务框架,主要用于Go。 但是,您可以使用Sidecar与其他语言进行交互。

Go-micro具有有用的功能,可用于在Go中创建微服务。 但是,我们可能首先从他解决的最常见问题开始,这就是发现服务。

为了使用go-micro,我们需要对我们的服务进行几次更新。 Go-micro集成为Protoc插件,在这种情况下,它替代了我们当前使用的标准gRPC插件。 因此,让我们从在Makefile中替换它开始。

确保安装go-micro依赖项:

 go get -u github.com/micro/protobuf/{proto,protoc-gen-go} 

更新我们的Makefile以使用go-micro插件而不是gRPC插件:

 build: protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto GOOS=linux GOARCH=amd64 go build docker build -t consignment . run: docker run -p 50051:50051 shippy-service-consignment 

现在我们需要更新我们的shippy-service-consignment / main.go以使用go-micro。 这抽象了我们以前的大多数gRPC代码。 它可以轻松地处理注册并加速服务的编写。

shippy服务寄售/ main.go
 // shippy-service-consignment/main.go package main import ( "fmt" //  protobuf  pb "github.com/EwanValentine/shippy/consignment-service/proto/consignment" "github.com/micro/go-micro" "context" ) //repository -   type repository interface { Create(*pb.Consignment) (*pb.Consignment, error) GetAll() []*pb.Consignment } // Repository -    , //       type Repository struct { consignments []*pb.Consignment } func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { updated := append(repo.consignments, consignment) repo.consignments = updated return consignment, nil } func (repo *Repository) GetAll() []*pb.Consignment { return repo.consignments } //         //       proto. //           . type service struct { repo repository } // CreateConsignment -        , //    create,      //     gRPC. func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, res *pb.Response) error { // Save our consignment consignment, err := s.repo.Create(req) if err != nil { return err } // Return matching the `Response` message we created in our // protobuf definition. res.Created = true res.Consignment = consignment return nil } //GetConsignments -         func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest, res *pb.Response) error { consignments := s.repo.GetAll() res.Consignments = consignments return nil } func main() { repo := &Repository{} //     Go-micro srv := micro.NewService( //           proto micro.Name("shippy.service.consignment"), ) // Init will parse the command line flags. srv.Init() //   pb.RegisterShippingServiceHandler(srv.Server(), &service{repo}) //   log.Println(" ") if err := srv.Run(); err != nil { fmt.Println(err) } } 


此处的主要更改是我们创建gRPC服务器的方式,该方法已从mico.NewService()巧妙地抽象出来,后者处理了我们的服务注册。 最后是service.Run()函数,该函数处理连接本身。 和以前一样,我们注册实现,但是这次使用的方法略有不同。

第二大变化涉及服务方法本身:对参数和响应类型进行了轻微修改,以接受请求和响应结构作为参数,现在仅返回错误。 在我们的方法中,我们设置了微处理的响应。

最后,我们不再对端口进行编程。 必须使用环境变量或命令行参数来配置Go-micro。 要设置地址,请使用MICRO_SERVER_ADDRESS =:50051。 默认情况下,Micro使用mdns(多播dns)作为服务发现代理供本地使用。 通常,您不使用mdns在生产环境中发现服务,但我们希望避免在本地运行Consul或etcd之类的东西进行测试。 稍后再详细介绍。

让我们更新Makefile来反映这一点。

 build: protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto GOOS=linux GOARCH=amd64 go build docker build -t consignment . run: docker run -p 50051:50051 \ -e MICRO_SERVER_ADDRESS=:50051 \ shippy-service-consignment 

-e是环境变量的标志,它允许您将环境变量传递到Docker容器。 每个变量都必须具有一个标志,例如-e ENV = staging -e DB_HOST = localhost,等等。

现在,如果您运行$ make run,您将拥有一个带有服务发现功能的Dockerized服务。 因此,让我们更新我们的Cli工具以使用它。

寄售
 package main import ( "encoding/json" "io/ioutil" "log" "os" "context" pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment" micro "github.com/micro/go-micro" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) func parseFile(file string) (*pb.Consignment, error) { var consignment *pb.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { service := micro.NewService(micro.Name("shippy.cli.consignment")) service.Init() client := pb.NewShippingServiceClient("shippy.service.consignment", service.Client()) // Contact the server and print out its response. file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("Could not parse file: %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("Could not greet: %v", err) } log.Printf("Created: %t", r.Created) getAll, err := client.GetConsignments(context.Background(), &pb.GetRequest{}) if err != nil { log.Fatalf("Could not list consignments: %v", err) } for _, v := range getAll.Consignments { log.Println(v) } } 


在这里,我们导入了go-micro库来创建客户端,并用go-micro客户端代码替换了现有的连接代码,该代码使用服务的权限而不是直接连接到地址。

但是,如果运行此命令,它将不起作用。 这是因为我们现在正在Docker容器中启动服务,该容器具有自己的mdns,与我们当前使用的mdns主机分开。 解决此问题的最简单方法是确保服务和客户端都在dockerland中运行,以便它们都在同一主机上工作并使用同一网络层。 因此,让我们创建make consignment-cli / Makefile并创建一些条目。

 build: GOOS=linux GOARCH=amd64 go build docker build -t shippy-cli-consignment . run: docker run shippy-cli-consignment 

和以前一样,我们要为Linux构建二进制文件。 当启动docker镜像时,我们想要传递一个环境变量,以发出go-micro命令来使用mdns。

现在让我们为CLI工具创建一个Dockerfile:

 FROM alpine:latest RUN mkdir -p /app WORKDIR /app ADD consignment.json /app/consignment.json ADD consignment-cli /app/consignment-cli CMD ["./shippy-cli-consignment"] 

这与我们的服务Dockerfile非常相似,除了它还会提取我们的json数据文件。

现在,当在shippy-cli-consignment中运行$ make run时,您应该像以前一样看到Created:true。

现在,看来该看看新的Docker功能了:多阶段构建。 这使我们可以在一个Dockerfile中使用多个Docker映像。

这在我们的情况下特别有用,因为我们可以使用一个映像来创建具有所有正确依赖项的二进制文件。 然后使用第二个图像启动它。 让我们尝试一下,我将在代码中留下详细的注释:
寄售服务/ Dockerfile
 # consignment-service/Dockerfile #     golang,    #     .    `as builder`, #     ,      . FROM golang:alpine as builder RUN apk --no-cache add git #         gopath WORKDIR /app/shippy-service-consignment #       COPY . . RUN go mod download #     ,   #       Alpine. RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-consignment #      FROM, #   Docker        . FROM alpine:latest # ,    -     RUN apk --no-cache add ca-certificates #   ,     . RUN mkdir /app WORKDIR /app #   ,       , #         `builder` #       , #    ,    , #      . ! COPY --from=builder /app/shippy-service-consignment/shippy-service-consignment . #     !        #        # run time . CMD ["./shippy-service-consignment"] 


现在,我将继续使用其他Docker文件并采用这种新方法。 哦,别忘了从您的Makefile中删除$ go build!

船舶服务


让我们创建第二个服务。 我们有一项服务(shippy-service-consignment),该服务负责将这批集装箱与船舶进行协调,这最适合此批货物。 为了匹配我们的批次,我们必须将集装箱的重量和数量发送到我们的新船服务部门,后者将找到能够处理该批次的船只。

$ mkdir vessel-service根目录中创建一个新目录,现在为我们的新protobuf服务定义$ mkdir -p shippy-service-vessel / proto / vessel创建一个子目录。 现在让我们创建一个新的protobuf文件, $ touch shippy-service-vessel / proto / vessel / vessel。vessel

由于protobuf的定义确实是我们软件设计的核心,因此让我们开始吧。

容器/器皿
 // shippy-service-vessel/proto/vessel/vessel.proto syntax = "proto3"; package vessel; service VesselService { rpc FindAvailable(Specification) returns (Response) {} } message Vessel { string id = 1; int32 capacity = 2; int32 max_weight = 3; string name = 4; bool available = 5; string owner_id = 6; } message Specification { int32 capacity = 1; int32 max_weight = 2; } message Response { Vessel vessel = 1; repeated Vessel vessels = 2; } 


如您所见,这与我们的第一项服务非常相似。 我们使用一个称为FindAvailable的rpc方法创建服务。 这采用一种规范类型,并返回一种响应类型。 响应类型使用重复字段返回“容器”类型或多个容器。

现在,我们需要创建一个Makefile来处理构建逻辑和启动脚本。 $ touch shippy-service-vessel / Makefile 。 打开此文件并添加以下内容:

 // vessel-service/Makefile build: protoc -I. --go_out=plugins=micro:. \ proto/vessel/vessel.proto docker build -t shippy-service-vessel . run: docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 shippy-service-vessel 

这几乎与我们为寄售服务创建的第一个Makefile相同,但是请注意,服务和端口的名称有所变化。 我们无法在同一端口上运行两个扩展坞容器,因此我们使用Dockers端口转发,以便该服务从主机网络上的50051重定向到50052。

现在,我们需要使用新的多阶段格式的Dockerfile:

 # vessel-service/Dockerfile FROM golang:alpine as builder RUN apk --no-cache add git WORKDIR /app/shippy-service-vessel COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-vessel FROM alpine:latest RUN apk --no-cache add ca-certificates RUN mkdir /app WORKDIR /app COPY --from=builder /app/shippy-service-vessel . CMD ["./shippy-service-vessel"] 

最后,我们可以编写实现:

船只服务/ main.go
 // vessel-service/main.go package main import ( "context" "errors" "fmt" pb "github.com/EwanValentine/shippy/vessel-service/proto/vessel" "github.com/micro/go-micro" ) type Repository interface { FindAvailable(*pb.Specification) (*pb.Vessel, error) } type VesselRepository struct { vessels []*pb.Vessel } // FindAvailable -     , //           , //      . func (repo *VesselRepository) FindAvailable(spec *pb.Specification) (*pb.Vessel, error) { for _, vessel := range repo.vessels { if spec.Capacity <= vessel.Capacity && spec.MaxWeight <= vessel.MaxWeight { return vessel, nil } } //     return nil, errors.New("     ") } //    grpc type service struct { repo repository } func (s *service) FindAvailable(ctx context.Context, req *pb.Specification, res *pb.Response) error { //     vessel, err := s.repo.FindAvailable(req) if err != nil { return err } //       res.Vessel = vessel return nil } func main() { vessels := []*pb.Vessel{ &pb.Vessel{Id: "vessel001", Name: "Boaty McBoatface", MaxWeight: 200000, Capacity: 500}, } repo := &VesselRepository{vessels} srv := micro.NewService( micro.Name("shippy.service.vessel"), ) srv.Init() //    pb.RegisterVesselServiceHandler(srv.Server(), &service{repo}) if err := srv.Run(); err != nil { fmt.Println(err) } } 


现在,让我们继续进行有趣的部分。 创建托运货物时,我们需要更改货物装卸服务以联系船搜索服务,找到船并更新所创建托运货物中的ship_id参数:

运货船/寄售服务/ main.go
 package main import ( "context" "fmt" "log" "sync" pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment" vesselProto "github.com/EwanValentine/shippy-service-vessel/proto/vessel" "github.com/micro/go-micro" ) const ( port = ":50051" ) type repository interface { Create(*pb.Consignment) (*pb.Consignment, error) GetAll() []*pb.Consignment } // Repository -    , //       type Repository struct { mu sync.RWMutex consignments []*pb.Consignment } //Create -     func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) { repo.mu.Lock() updated := append(repo.consignments, consignment) repo.consignments = updated repo.mu.Unlock() return consignment, nil } //GetAll -       func (repo *Repository) GetAll() []*pb.Consignment { return repo.consignments } //         //       proto. //            type service struct { repo repository vesselClient vesselProto.VesselServiceClient } // CreateConsignment -         create, //     ,     gRPC. func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, res *pb.Response) error { //         , //    vesselResponse, err := s.vesselClient.FindAvailable(context.Background(), &vesselProto.Specification{ MaxWeight: req.Weight, Capacity: int32(len(req.Containers)), }) log.Printf(" : %s \n", vesselResponse.Vessel.Name) if err != nil { return err } //     id  req.VesselId = vesselResponse.Vessel.Id //      consignment, err := s.repo.Create(req) if err != nil { return err } res.Created = true res.Consignment = consignment return nil } // GetConsignments -         func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest, res *pb.Response) error { consignments := s.repo.GetAll() res.Consignments = consignments return nil } func main() { //   repo := &Repository{} //  micro srv := micro.NewService( micro.Name("shippy.service.consignment"), ) srv.Init() vesselClient := vesselProto.NewVesselServiceClient("shippy.service.vessel", srv.Client()) //      gRPC. pb.RegisterShippingServiceHandler(srv.Server(), &service{repo, vesselClient}) //   if err := srv.Run(); err != nil { fmt.Println(err) } } 


在这里,我们为船舶服务创建了一个客户端实例,该实例允许我们使用服务名称,即 shipy.service.vessel以客户端的身份调用船舶的服务,并与其方法进行交互。在这种情况下,只有一种方法(FindAvailable)。我们将批次重量与我们要运送的集装箱数量一起运送,作为运送服务的规格。这将向我们返回与此规范相对应的船只。

更新consignment-cli / consignment.json文件,删除硬编码的ship_id,因为我们要确认我们的船舶搜索服务正在运行。另外,让我们添加更多的容器并增加重量。例如:

 { "description": "  ", "weight": 55000, "containers": [ { "customer_id": "_001", "user_id": "_001", "origin": "--" }, { "customer_id": "_002", "user_id": "_001", "origin": "" }, { "customer_id": "_003", "user_id": "_001", "origin": "" } ] } 

现在在consignment-cli中运行$ make build && make您应该看到包含已创建商品清单的答案。在您的聚会中,您应该看到已经设置了vessel_id参数。

因此,我们有两个互连的微服务和一个命令行界面!
在本系列的下一部分中,我们将考虑使用MongoDB保存其中一些数据。我们还将添加第三项服务,并使用docker-compose在本地管理我们不断发展的容器生态系统。

第一部分
EwanValentine原始存储库

Source: https://habr.com/ru/post/zh-CN455812/


All Articles