『GCTT 出品』Golang 中的微服务 - 第二部分 - Docker 和 go-micro
简介: Docker 和 go-micro
在上篇文章中,我们大致介绍了如何编写一个基于 gRPC
的微服务。在这个部分,我们将涵盖 Docker
服务的基础知识,我们也将使用 go-micro 更新我们的服务,并在文本末尾引入第二个服务。
Docker简介
随着云计算的到来和微服务的诞生,服务在部署的时候有更多的压力,但是一次一小段代码就产生了一些有趣的新思想和新技术,其中之一就是容器的概念。
在早些的时候,团队部署一个庞大的服务到静态服务器,运行一套操作系统,需要使用一组预定义的依赖来跟踪。例如,可能是由管理员提供的 VM
虚拟机或者 Puppet
。伸缩是昂贵的并且不一定有效,最常见的是垂直缩放,例如在静态服务器上投入越来越多资源。
针对虚拟机的配置,伴随着像 vagrant 这样的工具越来越常使用。但是运行一个虚拟机仍然是一个相当大的操作。它相当于在你的主机上运行一个完整的操作系统(包括内核,各种应用等)。在资源方面,这是相当昂贵的。所以当微服务出现时,让每个微服务独立跑在自己的虚拟机中变得不可行了。
容器的诞生
容器是精简版的操作系统。容器不包含内核、用户操作系统或通常构成操作系统的较低级别组件。
容器只包含顶层库及其运行组件,内核在主机上共享。所以主机运行一个 Unix
内核,然后由 n
个容器共享,运行非常不同的运行时集合。
在底层,容器使用各种内核工具。以便跨容器空间共享资源和网络功能。
进一步阅读
这意味着您可以运行代码所需的运行时和依赖关系,无需启动几个完整的操作系统。这改变了游戏规划,因为一个容器和虚拟机比较体积是比较小的。例如 Ubuntu
,它通常 1GB
小一点,而 Docker
镜像只有 188M
。
你会注意到我在这个介绍中更广泛的谈到容器,而不是 Docker
容器。尽管人们通常认为 Docker 和容器是一回事。但是,容器在 Linux 中更多是一个概念或一组功能。
Docker 只是其中的一种,只是因为好用而变得流行,还有其他的。但是我们会专注于 Docker
,因为在我看来它是目前支持度最高且对新手最友好的容器技术。。
所以希望你看到容器的价值。我们开始使用 Docker
来运行我们的第一个服务。我们来创建一个 Dockerfile
。(译注: Docker 容器的创建一般都使用 Dockerfile,容器会根据这个文件创建相对应的运行环境)
touch consignment-service/Dockerfile
在该文件中添加以下内容:
FROM alpine:latest
RUN mkdir /app
WORKDIR /app
ADD consignment-service /app/consignment-service
CMD ["./consignment-service"]
如果你在 Linux 上运行,你可能会遇到使用 Alpine 的问题。所以,如果你在 Linux 机器上关注这篇文章,只需用 debian
替换 alpine
即可,你应该能够正常运行。 稍后我们将会介绍一个更好的方法来构建我们的二进制文件。
首先,我们下载最新的 Linux Alpine 镜像。Linux Alpine 是一个轻量级 Linux 发行版,为运行 Dockerised Web 应用程序而开发和优化。换一种说法,Linux Alpine 具有足够的依赖和运行时功能来运行大多数应用程序。这意味着它的镜像大小只有 8M 左右。与之相比,大约 1GB 的 Ubuntu 虚拟机,你可以开始看到为什么 Docker 镜像更适合微服务和云计算。
接下来我们创建一个新的目录来存放我们的应用程序,并将上下文目录设置到我们的新目录中。这时我们的应用程序目录是默认的目录。然后,我们将编译后的二进制文件添加到我们的 Docker 容器中,并运行它。
现在我们来更新 Makefile
文件来构建我们的 Docker 镜像。
build:
...
GOOS=linux GOARCH=amd64 go build
docker build -t consignment-service .
我们在这里增加了两个步骤,我想详细解释一下。首先,我们正在构建我们的二进制文件。你会注意到在运行命令 $ go build
之前,设置了两个环境变量。GOOS 和 GOARCH 允许您为另一个操作系统交叉编译您的二进制文件,由于我在 Macbook上开发,所以无法编译出二进制文件,让它在 Docker 容器中运行它,而该容器使用的是 Linux。这个二进制在你的 Docker 容器中将是完全没有意义的,它会抛出一个错误。第二步是添加 Docker 构建过程。这将读取你的 Dockerfile 文件,并通过一个名称 consignment-service
构建镜像。句号表示一个目录路径,在这里我们只是希望构建过程在当前目录中查找。
我将在我们的 Makefile 中添加一个新条目:
run:
docker run -p 50051:50051 consignment-service
在这里,我们运行 consignment-service
Docker 镜像,并暴露 50051 端口。由于 Docker 在单独的网络层上运行,因此您需要将 Docker 容器中使用的端口转发给主机。您可以通过更改第一个段将内部端口转发到主机上的新端口。例如,如果要在端口 8080 上运行此服务,则需要将 -p 参数更改为 8080:50051
。您也可以通过包含 -d
标志在后台运行容器。例如,docker run -d -p 50051:50051 consignment-service
。
您可以阅读更多关于 Docker 网络如何工作的信息。
当您运行 $ docker build
时,您正在将代码和运行时环境构建到镜像中。Docker 镜像是您的环境及其依赖关系的可移植快照。你可以将它分享到 Docker Hub 来共享你的 Docker 镜像。Docker 镜像就像一个 npm 或 yum repo。当你在你的 Dockerfile 里面定义了 FROM
,你就告诉了 docker 从 docker hub 下载哪个镜像来作为运行环境。然后,您可以扩展并覆盖该基本文件的某些部分,方法是自行重新定义它们。我们不会公开我们的 Docker 镜像,但是可以随时仔细阅读 Docker hub,并且注意到有多少功能被容器化。一些非常显著的事情已经被 Docker 化了。
Dockerfile 中的每个声明在第一次构建时都被缓存。这样可以节省每次更改时重新构建整个运行时的时间。 Docker 非常聪明,可以确定哪些部分发生了变化,哪些部分需要重新构建。这使得构建过程非常快速。
我们已经介绍了很多容器的部分了。让我们回到我们的代码。
在创建 gRPC 服务时,创建连接的代码有很多,并且必须将服务地址的位置硬编码到客户端或其他服务中,以便连接到它。这很棘手,因为当您在云中运行服务时,它们可能不共享相同的主机,或者重新部署服务后地址或 IP 可能会更改。
这是服务发现的起点。服务发现保持所有服务及其位置的最新目录。每个服务在运行时注册自己,并在关闭时自行注销。 每个服务都有一个名字或编号分配给它。 因此,即使可能有新的 IP 地址或主机地址,只要服务名称保持不变,您就不需要从其他服务更新对此服务的调用。
通常情况下,解决这个问题的方法有很多,但是像编程中的大多数情况一样,如果已经有人解决了这个问题,那么重新发明轮子就没有意义了。 Go-micro 的创始人是 @chuhnk(Asim Aslam),他以一种非常清晰和易用的方式解决了这些问题。
Go-micro
Go-micro 是一个用 Go 编写的强大的微服务框架,大部分用于 Go。但是,您也可以使用 Sidecar 以便与其他语言进行交互。
Go-micro 有一些有用的功能,可以用来制作微型服务。但是,我们将从可能解决的最常见问题开始,那就是服务发现。
我们需要对我们的服务进行一些更新,让它与 go-micro 一起工作。Go-micro 作为 protoc 插件集成,在这种情况下,替换我们当前使用的标准 gRPC 插件。所以让我们开始在我们的 Makefile 中替换它。
确保安装 go-micro 依赖:
go get -u github.com/micro/protobuf/{proto,protoc-gen-go}
build:
protoc -I. --go_out=plugins=micro:$(GOPATH)/src/github.com/EwanValentine/shippy/consignment-service \
proto/consignment/consignment.proto
...
...
我们已经更新了我们的 Makefile 来使用 go-micro 插件,而不是 gRPC 插件。现在需要更新我们的 consignment-service/main.go
文件来使用 go-micro。这将抽象我们以前的 gRPC 代码,它将处理注册和轻松启动我们的服务
// consignment-service/main.go
package main
import (
// Import the generated protobuf code
"fmt"
pb "github.com/EwanValentine/shippy/consignment-service/proto/consignment"
micro "github.com/micro/go-micro"
"golang.org/x/net/context"
)
type IRepository interface {
Create(*pb.Consignment) (*pb.Consignment, error)
GetAll() []*pb.Consignment
}
// Repository - Dummy repository, this simulates the use of a datastore
// of some kind. We'll replace this with a real implementation later on.
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
}
// Service should implement all of the methods to satisfy the service
// we defined in our protobuf definition. You can check the interface
// in the generated code itself for the exact method signatures etc
// to give you a better idea.
type service struct {
repo IRepository
}
// CreateConsignment - we created just one method on our service,
// which is a create method, which takes a context and a request as an
// argument, these are handled by the gRPC server.
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
}
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{}
// Create a new service. Optionally include some options here.
srv := micro.NewService(
// This name must match the package name given in your protobuf definition
micro.Name("go.micro.srv.consignment"),
micro.Version("latest"),
)
// Init will parse the command line flags.
srv.Init()
// Register handler
pb.RegisterShippingServiceHandler(srv.Server(), &service{repo}) // Run the server
if err := srv.Run(); err != nil {
fmt.Println(err)
}
}
这里的主要变化是我们实例化我们的 gRPC 服务器的方式,它处理注册我们的服务,已经被整齐地抽象到一个 mico.NewService()
方法中。最后,处理连接本身的 service.Run()
函数。和以前一样,我们注册了我们的实现,但这次使用了一个稍微不同的方法。第二个最大的变化是服务方法本身,参数和响应类型略有变化,把请求和响应结构作为参数,现在只返回一个错误。在我们的方法中,设置了由 go-micro
处理响应。
最后,我们不再对端口进行硬编码。go-micro 应该使用环境变量或命令行参数进行配置。设置地址, 使用 MICRO_SERVER_ADDRESS=:50051
。我们还需要告诉我们的服务使用 mdns(多播DNS)作为我们本地使用的服务代理。
您通常不会在生产环境中使用 mdns 进行服务发现,但我们希望避免在本地运行诸如 Consul 或 etcd 这样的测试。更多我们将在后面介绍。
让我们更新我们的 Makefile 来实现这一点。
run:
docker run -p 50051:50051 \
-e MICRO_SERVER_ADDRESS=:50051 \
-e MICRO_REGISTRY=mdns consignment-service
-e
是一个环境变量标志,它允许你将环境变量传递到你的 Docker 容器中。
每个变量必须有一个标志,例如 -e ENV=staging -e DB_HOST=localhost
等。
现在如果你运行 make run
,您将拥有一个 Dockerised 服务,并具有服务发现功能。所以让我们更新我们的 cli 工具来利用它。
import (
... "github.com/micro/go-micro/cmd"
microclient "github.com/micro/go-micro/client"
)
func main() {
cmd.Init()
// Create new greeter client
client := pb.NewShippingServiceClient("go.micro.srv.consignment", microclient.DefaultClient)
...
}
完整文件看这里
在这里,我们导入了用于创建客户端的 go-micro 库,并用 go-micro 客户端代码取代了现有的连接代码,该客户端代码使用服务解析而不是直接连接到地址。
但是,如果你运行它,这是行不通的。这是因为我们现在正在 Docker 容器中运行我们的服务,它有自己的 mdns,独立于我们使用中的主机 mdns 。解决这个问题的最简单的方法是确保服务和客户端都在 “dockerland” 中运行,以便它们都在相同的主机上运行,并使用相同的网络层。让我们创建一个 Makefile consignment-cli/Makefile
,并创建一些条目。
build:
GOOS=linux GOARCH=amd64 go build
docker build -t consignment-cli .
run:
docker run -e MICRO_REGISTRY=mdns consignment-cli
与之前类似,我们要为 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 ["./consignment-cli"]
它除了引入了我们的 json 数据文件,其余与之前服务的 Dockerfile 非常相似。如果你在你的 consignment-cli
目录,运行 $ make run
命令,你应该和以前一样,看见 Created: true
。
之前,我提到那些使用 Linux 的人应该切换到使用 Debian 作为基础映像。现在看起来是一个很好的时机来看看 Docker 的一个新功能:多阶段构建。这使我们可以在一个 Dockerfile 中使用多个 Docker 镜像。
这在我们的例子中尤其有用,因为我们可以使用一个镜像来构建我们的二进制文件,具有所有正确的依赖关系等,然后使用第二个镜像来运行它。让我们试试看,我会在代码中留下详细的注释:
# consignment-service/Dockerfile
# We use the official golang image, which contains all the
# correct build tools and libraries. Notice `as builder`,
# this gives this container a name that we can reference later on.
FROM golang:1.9.0 as builder
# Set our workdir to our current service in the gopath
WORKDIR /go/src/github.com/EwanValentine/shippy/consignment-service
# Copy the current code into our workdir
COPY . .
# Here we're pulling in godep, which is a dependency manager tool,
# we're going to use dep instead of go get, to get around a few
# quirks in how go get works with sub-packages.
RUN go get -u github.com/golang/dep/cmd/dep
# Create a dep project, and run `ensure`, which will pull in all
# of the dependencies within this directory.
RUN dep init && dep ensure
# Build the binary, with a few flags which will allow
# us to run this binary in Alpine.
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
# Here we're using a second FROM statement, which is strange,
# but this tells Docker to start a new build process with this
# image.
FROM alpine:latest
# Security related package, good to have.
RUN apk --no-cache add ca-certificates
# Same as before, create a directory for our app.
RUN mkdir /app
WORKDIR /app
# Here, instead of copying the binary from our host machine,
# we pull the binary from the container named `builder`, within
# this build context. This reaches into our previous image, finds
# the binary we built, and pulls it into this container. Amazing!
COPY --from=builder /go/src/github.com/EwanValentine/shippy/consignment-service/consignment-service .
# Run the binary as per usual! This time with a binary build in a
# separate container, with all of the correct dependencies and
# run time libraries.
CMD ["./consignment-service"]
这种方法的唯一问题,我想回来并在某些时候改善这一点,是 Docker 不能从父目录中读取文件。它只能读取 Dockerfile 所在目录或子目录中的文件。
这意味着为了运行 $ dep ensure
或 $ go get
,你需要确保你的代码被推到 Git 上,这样它就可以提取 vessel-service。就像其他 Go 包一样。这种方法不理想,但足够满足我们现在的需求。
现在我将会在其他 Docker 文件中应用这种新方法。
噢,记住要记得从 Makefiles 中删除 $ go build
。
更多的多阶段编译在这里
Vessel 服务
让我们创建第二个服务。我们有一个代销服务,这将处理将一批货物与一批最适合该批货物的 vessel 进行匹配。为了配合我们的货物,我们需要将集装箱的重量和数量发送到我们的新 vessel 服务,然后将找到一个能够处理该货物的船只。
在你的根目录创建一个新的目录 $ mkdir vessel-service
。现在为我们的新服务 protobuf 定义创建了一个子目录, $ mkdir -p vessel-service/proto/vessel
。现在让我们来创建新的 protobuf
文件, $ touch vessel-service/proto/vessel/vessel.proto
。
由于 protobuf 的定义确实是我们领域设计的核心,所以我们从这里开始。
// vessel-service/proto/vessel/vessel.proto
syntax = "proto3";
package go.micro.srv.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;
}
正如你所看到的,这和我们的第一个服务非常相似。我们用一个 rpc
方法 FindAvailable
创建了一个服务。。这需要一个 Specification 类型并返回一个 Response 类型。Response 类型使用重复字段返回 Vessel 类型或多个 Vesse。
现在我们需要创建一个 Makefile 来处理我们的构建逻辑和运行脚本。 $ touch vessel-service/Makefile
。打开该文件并添加以下内容:
// vessel-service/Makefile
build:
protoc -I. --go_out=plugins=micro:$(GOPATH)/src/github.com/EwanValentine/shippy/vessel-service \
proto/vessel/vessel.proto
docker build -t vessel-service .
run:
docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 -e MICRO_REGISTRY=mdns vessel-service
这与我们为托管服务创建的第一个 Makefile 几乎相同,但注意服务名称和端口已经改变了一点。我们不能在同一个端口上运行两个 Docker 容器,所以我们在这里利用 Docker 端口转发来确保服务上的 50051 端口映射到主机网络上的 50052 端口。
现在我们需要一个 Dockerfile,使用我们新的多阶段格式:
# vessel-service/Dockerfile
FROM golang:1.9.0 as builder
WORKDIR /go/src/github.com/EwanValentine/shippy/vessel-service
COPY . .
RUN go get -u github.com/golang/dep/cmd/dep
RUN dep init && dep ensure
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
RUN mkdir /app
WORKDIR /app
COPY --from=builder /go/src/github.com/EwanValentine/shippy/vessel-service/vessel-service .
CMD ["./vessel-service"]
最后,我们可以开始我们的实现。
// 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 - checks a specification against a map of vessels,
// if capacity and max weight are below a vessels capacity and max weight,
// then return that vessel.
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("No vessel found by that spec")
}
// Our grpc service handler
type service struct {
repo Repository
}
func (s *service) FindAvailable(ctx context.Context, req *pb.Specification, res *pb.Response) error {
// Find the next available vessel
vessel, err := s.repo.FindAvailable(req)
if err != nil {
return err
}
// Set the vessel as part of the response message type
res.Vessel = vessel
return nil
}
func main() {
vessels := []*pb.Vessel{
&pb.Vessel{Id: "vessel001", Name: "Kane's Salty Secret", MaxWeight: 200000, Capacity: 500},
}
repo := &VesselRepository{vessels}
srv := micro.NewService(
micro.Name("go.micro.srv.vessel"),
micro.Version("latest"),
)
srv.Init()
// Register our implementation with
pb.RegisterVesselServiceHandler(srv.Server(), &service{repo})
if err := srv.Run(); err != nil {
fmt.Println(err)
}
}
我留下了一些注释,但是非常简单。另外,我想提下,一个 Reddit 用户 /r/jerky_lodash46 指出,我曾经使用 IRepository 作为我的接口名称。我想在这里纠正一下,在我的接口名前面加上 Java 和 C# 等语言的约定,但 Go 并没有真正鼓励这一点,因为 Go 把接口当作一等公民。所以我把 IRepository 更名为 Repository ,并且把我的具体结构重命名为 ConsignmentRepository。
这个系列中,我会留下任何错误,并在以后的文章中予以纠正,以便我能够解释这些改进。我们可以更多地学习。
现在让我们来看看有趣的部分。当我们创建一个托运货物时,我们需要改变我们的托运服务来呼叫我们的新 vessel 服务,找到一艘船,并更新创建的托运中的 vessel_id:
// consignment-service/main.go
package main
import ( // Import the generated protobuf code
"fmt"
"log"
pb "github.com/EwanValentine/shippy/consignment-service/proto/consignment"
vesselProto "github.com/EwanValentine/shippy/vessel-service/proto/vessel"
micro "github.com/micro/go-micro"
"golang.org/x/net/context"
)
type Repository interface {
Create(*pb.Consignment) (*pb.Consignment, error)
GetAll() []*pb.Consignment
}
// Repository - Dummy repository, this simulates the use of a datastore
// of some kind. We'll replace this with a real implementation later on.
type ConsignmentRepository struct {
consignments []*pb.Consignment
}
func (repo *ConsignmentRepository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
updated := append(repo.consignments, consignment)
repo.consignments = updated
return consignment, nil
}
func (repo *ConsignmentRepository) GetAll() []*pb.Consignment {
return repo.consignments
}
// Service should implement all of the methods to satisfy the service
// we defined in our protobuf definition. You can check the interface
// in the generated code itself for the exact method signatures etc
// to give you a better idea.
type service struct {
repo Repository
vesselClient vesselProto.VesselServiceClient
}
// CreateConsignment - we created just one method on our service,
// which is a create method, which takes a context and a request as an
// argument, these are handled by the gRPC server.
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, res *pb.Response) error {
// Here we call a client instance of our vessel service with our consignment weight,
// and the amount of containers as the capacity value
vesselResponse, err := s.vesselClient.FindAvailable(context.Background(), &vesselProto.Specification{
MaxWeight: req.Weight,
Capacity: int32(len(req.Containers)),
})
log.Printf("Found vessel: %s \n", vesselResponse.Vessel.Name)
if err != nil {
return err
}
// We set the VesselId as the vessel we got back from our
// vessel service
req.VesselId = vesselResponse.Vessel.Id
// 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
}
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 := &ConsignmentRepository{}
// Create a new service. Optionally include some options here.
srv := micro.NewService(
// This name must match the package name given in your protobuf definition
micro.Name("go.micro.srv.consignment"),
micro.Version("latest"),
)
vesselClient := vesselProto.NewVesselServiceClient("go.micro.srv.vessel", srv.Client())
// Init will parse the command line flags.
srv.Init()
// Register handler
pb.RegisterShippingServiceHandler(srv.Server(), &service{repo, vesselClient})
// Run the server
if err := srv.Run(); err != nil {
fmt.Println(err)
}
}
在这里,我们为我们的 vessel 服务创建了一个客户端实例,这允许我们使用服务名称,即 go.micro.srv.vessel 将船舶服务作为客户端调用,并与其方法交互。在这种情况下,只有一个方法 (FindAvailable
)。我们把我们的寄售重量,以及我们想要作为规格的集装箱的数量发送到 vessel 服务。然后返回一个适当的 vessel。
更新 consignment-cli/consignment.json
文件,删除硬编码的 vessel_id ,我们要确认我们自己正在工作。让我们再添加一些容器,增加权重。例如
{
"description": "This is a test consignment",
"weight": 55000,
"containers": [
{ "customer_id": "cust001", "user_id": "user001", "origin": "Manchester, United Kingdom" },
{ "customer_id": "cust002", "user_id": "user001", "origin": "Derby, United Kingdom" },
{ "customer_id": "cust005", "user_id": "user001", "origin": "Sheffield, United Kingdom" }
]}
现在在 consignment-cli
中运行 $ make build && make
。您应该看到一个响应,并创建货物清单。在您的货物中,您现在应该看到 vessel_id 已经设置好了。所以现在有了我们两个互联的微服务和一个命令行界面!在这个系列的下一部分,我们将看看使用 MongoDB 来保存这些数据。我们还将添加第三个服务,并使用 docker-compose
来管理我们在本地不断增长的容器生态系统。
查看完整示例的代码。如有任何反馈,请将其发送至(mailto:ewan.valentine89@gmail.com)。非常感激!
如果你发现这个系列很有用,而且你使用了一个广告拦截器(谁可以责怪你)。请考虑一下我的时间和精力。干杯! https://monzo.me/ewanvalentine
Docker 简报(2017 年 11 月 22 日)
via: https://ewanvalentine.io/microservices-in-golang-part-2/
作者:Ewan Valentine
译者:guoxiaopang
校对:QueShengyao polaris1119
本文由 GCTT 原创编译,Go 中文网 荣誉推出
喜欢本文的朋友们,欢迎长按下图关注订阅号Go语言中文网,收看更多精彩内容
微信分享/微信扫码阅读