前言

如果你是一个 Go 开发者,go-kit 为开发者提供了一套抽象,包和接口,这样你实现的服务就可以标准化。

我想开始一个使用 go-kit 工具的深入教程。我们将创建一个微服务系统,设置环境,重温服务件交互的逻辑。

我们将用以下几个微服务创建一个 bug 追踪系统:

  • Users
  • Bugs
  • Notificator

创建服务

下载安装包:

1
2
go get github.com/go-kit/kit
go get github.com/kujtimiihoxha/kit

创建以下服务:

1
2
3
kit new service users
kit new service bugs
kit new service notificator

这些命令将生成初始的文件夹结构和服务接口。接口默认为空,让我们定义接口中的函数。从创建用户函数开始。

users:

1
2
3
4
5
6
7
8
package service

import "context"

// 用户服务
type UsersService interface {
    Create(ctx context.Context, email string) error
}

bugs:

1
2
3
4
5
6
7
8
9
package service

import "context"

// bug服务
type BugsService interface {
    // 在这里添加你自己的方法
    Create(ctx context.Context, bug string) error
}

notifcator:

1
2
3
4
5
6
7
8
9
package service

import "context"

// 通知服务
type NotificatorService interface {
    // 在这里添加你自己的方法
    SendEmail(ctx context.Context, email string, content string) error
}

生成HTTP服务

然后我们需要运行一个命令生成一个服务,这个命令将创建服务模板,服务中间件和终端代码。同时它还创建了一个 cmd/ 包来运行我们的服务。

1
2
kit generate service users --dmw
kit generate service bugs --dmw

-dmw 参数创建默认的终端中间件和日志中间件。

这个命令已经将 go-kit 工具的端点包和 HTTP 传输包添加到我们的代码中。现在我们只需要在任意一个位置实现我们的业务代码就可以了。

生成Dockerfile

go-kit CLI 还可以创建 docker-compose 模板,让我们试一试。

1
kit generate docker

这个命令生成了 Dockerfile 和带有端口映射的 docker-compose.yml 。将它运行起来并调用 /create 接口 。

1
docker-compose up

Dockerfiles 使用了 go 的 watcher 包,如果 Go 代码发生了变化,它会更新并重启二进制文件,这在本地环境中非常方便。

现在我们的服务运行的端口是 8800,8801,8802。可以试着调用用户服务:

1
curl -XPOST http://localhost:8800/create -d '{"email": "test"}'

生成GRPC服务

因为通知服务是一个内部服务,所以不需要 REST API,我们用 gRPC 来实现它.

为此,我们需要先安装 protoc 和 protobuf 。

1
kit generate service notificator -t grpc --dmw

这同时生成了 .pb 文件.

让我们首先通过编写原型定义来实现 Notificator 服务,它是一个 gRPC 服务。我们已经生成了 notificator/pkg/grpc/pb/notificator.pb 文件,让我们简单的去实现它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
syntax = "proto3";

package pb;

service Notificator {
    rpc SendEmail (SendEmailRequest) returns (SendEmailReply);
}

message SendEmailRequest {
    string email = 1;
    string content = 2;
}

message SendEmailReply {
    string id = 1;
}

现在我们要生成服务端和客户端的endpoint,可以使用 kit 工具生成的 compile.sh 脚本,脚本已经包含 protoc 命令。

1
2
cd notificator/pkg/grpc/pb
./compile.sh

可以发现,notificator.pb.go 这个文件已经更新了。

现在我们需要实现服务本身了。我们用生成一个 UUID 代替发送真实的电子邮件。首先需要对服务进行调整以匹配我们的请求和响应格式(返回一个新的参数:id)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
notificator/pkg/service/service.go:

import (
    "context"

    uuid "github.com/satori/go.uuid"
)

// NotificatorService describes the service.
type NotificatorService interface {
    // Add your methods here
    SendEmail(ctx context.Context, email string, content string) (string, error)
}

type basicNotificatorService struct{}

func (b *basicNotificatorService) SendEmail(ctx context.Context, email string, content string) (string, error) {
    id, err := uuid.NewV4()
    if err != nil {
        return "", err
    }

    return id.String(), nil
}

notificator/pkg/service/middleware.go:

1
2
3
4
5
6
func (l loggingMiddleware) SendEmail(ctx context.Context, email string, content string) (string, error) {
    defer func() {
        l.logger.Log("method", "SendEmail", "email", email, "content", content)
    }()
    return l.next.SendEmail(ctx, email, content)
}

notificator/pkg/endpoint/endpoint.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// SendEmailResponse 接受 SendEmail 方法的响应
type SendEmailResponse struct {
    Id string
    E0 error `json:"e0"`
}

// MakeSendEmailEndpoint 返回 SendEmail 调用的端点
func MakeSendEmailEndpoint(s service.NotificatorService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(SendEmailRequest)
        id, e0 := s.SendEmail(ctx, req.Email, req.Content)
        return SendEmailResponse{Id: id, E0: e0}, nil
    }
}

如果我们搜索 TODO grep -R “TODO” notificator ,可以看到还需要实现 gRPC 请求和响应的编码和解码。

notificator/pkg/grpc/handler.go:

1
2
3
4
5
6
7
8
9
func decodeSendEmailRequest(_ context.Context, r interface{}) (interface{}, error) {
    req := r.(*pb.SendEmailRequest)
    return endpoint.SendEmailRequest{Email: req.Email, Content: req.Content}, nil
}

func encodeSendEmailResponse(_ context.Context, r interface{}) (interface{}, error) {
    reply := r.(endpoint.SendEmailResponse)
    return &pb.SendEmailReply{Id: reply.Id}, nil
}

服务发现

SendEmail 接口将由用户服务调用,所以用户服务必须知道通知服务的地址,这是典型的服务发现问题。在本地环境中,我们使用 Docker Compose 部署时知道如何连接服务,但是,在实际的分布式环境中,可能会遇到其他问题。

首先,在 etcd 中注册我们的通知服务。etcd 是一种可靠的分布式键值存储,广泛应用于服务发现。go-kit 支持使用其他的服务发现技术如:eureka、consul 和 zookeeper 等。

把它添加进 Docker Compose 中,这样我们的服务就可以使用它了:

docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17


    etcd:
        image: 'quay.io/coreos/etcd:v3.1.7'
        restart: always
        ports:
            - '23791:2379'
            - '23801:2380'
        environment:
            ETCD_NAME: infra
            ETCD_INITIAL_ADVERTISE_PEER_URLS: 'http://etcd:2380'
            ETCD_INITIAL_CLUSTER: infra=http://etcd:2380
            ETCD_INITIAL_CLUSTER_STATE: new
            ETCD_INITIAL_CLUSTER_TOKEN: secrettoken
            ETCD_LISTEN_CLIENT_URLS: 'http://etcd:2379,http://localhost:2379'
            ETCD_LISTEN_PEER_URLS: 'http://etcd:2380'
            ETCD_ADVERTISE_CLIENT_URLS: 'http://etcd:2379'

在 etcd 中注册通知服务 notificator/cmd/service/service.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
registrar, err := registerService(logger)
if err != nil {
    logger.Log(err)
    return
}

defer registrar.Deregister()

func registerService(logger log.Logger) (*sdetcd.Registrar, error) {
    var (
        etcdServer = "http://etcd:2379"
        prefix     = "/services/notificator/"
        instance   = "notificator:8082"
        key        = prefix + instance
    )

    client, err := sdetcd.NewClient(context.Background(), []string{etcdServer}, sdetcd.ClientOptions{})
    if err != nil {
        return nil, err
    }

    registrar := sdetcd.NewRegistrar(client, sdetcd.Service{
        Key:   key,
        Value: instance,
    }, logger)

    registrar.Register()

    return registrar, nil
}

当我们的程序停止或者崩溃时,应该要记得取消注册。现在 etcd 已经知道我们的服务了,这个例子中只有一个实例,实际环境中显然会有更多。 现在让我们来测试一下通知服务是否注册到了 etcd 中:

1
2
docker-compose up -d etcd
docker-compose up -d notificator

现在我们使用用户服务调用通知服务,当我们创建一个服务后它会发送一个虚构的通知给用户。

由于通知服务是一个 gRPC 服务,在用户服务中,我们需要和客户端共用一个客户端endpoint。

protobuf 的客户端endpoint代码在 notificator/pkg/grpc/pb/notificator.pb.go ,我们把这个包引入我们的客户端

users/pkg/service/service.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

import (
    "github.com/plutov/packagemain/13-go-kit-2/notificator/pkg/grpc/pb"
    "google.golang.org/grpc"
)

type basicUsersService struct {
    notificatorServiceClient pb.NotificatorClient
}

func (b *basicUsersService) Create(ctx context.Context, email string) error {
    reply, err := b.notificatorServiceClient.SendEmail(context.Background(), &pb.SendEmailRequest{
        Email:   email,
        Content: "Hi! Thank you for registration...",
    })

    if reply != nil {
        log.Printf("Email ID: %s", reply.Id)
    }

    return err
}

// NewBasicUsersService 返回一个简单的、无状态的用户服务
func NewBasicUsersService() UsersService {
    conn, err := grpc.Dial("notificator:8082", grpc.WithInsecure())
    if err != nil {
        log.Printf("unable to connect to notificator: %s", err.Error())
        return new(basicUsersService)
    }

    log.Printf("connected to notificator")

    return &basicUsersService{
        notificatorServiceClient: pb.NewNotificatorClient(conn),
    }
}

当通知服务注册到 etcd 中时,我们可以用 etcd 的地址替换它的硬编码地址。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var etcdServer = "http://etcd:2379"

client, err := sdetcd.NewClient(context.Background(), []string{etcdServer}, sdetcd.ClientOptions{})
if err != nil {
    log.Printf("unable to connect to etcd: %s", err.Error())
    return new(basicUsersService)
}

entries, err := client.GetEntries("/services/notificator/")
if err != nil || len(entries) == 0 {
    log.Printf("unable to get prefix entries: %s", err.Error())
    return new(basicUsersService)
}

conn, err := grpc.Dial(entries[0], grpc.WithInsecure())

因为我们只有一个服务,所以只获取到了一个连接,但实际系统中可能有上百个服务,所以我们可以应用一些逻辑来进行实例选择,例如轮询。 现在让我们启动用户服务来测试一下:

1
docker-compose up users

调用 http 接口来创建一个用户:

1
curl -XPOST http://localhost:8802/create -d '{"email": "test"}'

转载:https://learnku.com/go/t/36960