前言
如果你是一个 Go 开发者,go-kit 为开发者提供了一套抽象,包和接口,这样你实现的服务就可以标准化。
我想开始一个使用 go-kit 工具的深入教程。我们将创建一个微服务系统,设置环境,重温服务件交互的逻辑。
我们将用以下几个微服务创建一个 bug 追踪系统:
创建服务
下载安装包:
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 模板,让我们试一试。
这个命令生成了 Dockerfile 和带有端口映射的 docker-compose.yml 。将它运行起来并调用 /create 接口 。
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