Golang 微服务教程(五)

原文链接:ewanvalentine.io,翻译已获作者 Ewan Valentine 授权。

本文完整代码:GitHub

在上节中,我们使用 JWT 在微服务之间进行了用户的认证。在本节中,我们将使用 go-micro 结合 nats 插件来完成用户创建事件的发布与订阅。

正如前几节所说,go-micro 是一个拔插式的框架,能与很多优秀的开源软件进行对接,可参考插件列表:go-plugins,可看到已支持很多优秀组件。

事件驱动

概念

事件驱动架构 理解起来比较简单,普遍认为好的软件架构都是解耦的,微服务之间不应该相互耦合或依赖。举个例子,我们在代码中调用微服务 go.srv.user-service 的函数,会先通过服务发现找到微服务的地址再调用,我们的代码与该微服务有了直接性的调用交互,并不算是完全的解耦。更多参考:软件架构入门

发布与订阅模式

为了理解事件驱动架构为何能使代码完全解耦,先了解事件的发布、订阅流程。微服务 X 完成任务 x 后通知消息系统说 “x 已完成”,它并不关心有哪些微服务正在监听这个事件、事件发生后会产生哪些影响。如果系统发生了某个事件,随之其他微服务都要做出动作是很容易的。

举个例子,user-service 创建了一个新用户,email-service 要给该用户发一封注册成功的邮件,message-service 要给网站管理员发一条用户注册的通知短信。

一般实现

在 user-service 的代码中实例化另两个微服务 Client 后,调用函数发邮件和短信,代码耦合度很高。如下图:

image-20180529201004910

事件驱动

在事件驱动的架构下,user-service 只需向消息系统发布一条 topic 为 “user.created” 的消息,其他两个订阅了此 topic 的 service 能知道有用户注册了,拿到用户信息后他们自行发邮件、发短信。如下图:

image-20180529200406346

本节中,我们将在 user-service 创建一个新用户时发布一个事件,使得 email-service 给用户发送邮件。

代码实现

go-micro NATS 插件

我们先将 NATS 插件集成到我们的代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// user-service/main.go

func main() {
...
// 初始化命令行环境
srv.Init()

// 获取 broker 实例
pubSub := srv.Server().Options().Broker

// 注册 handler
pb.RegisterUserServiceHandler(srv.Server(), &handler{repo, &t, pubSub})
...
}

需要注意的是,当 go-micro 创建微服务时,srv.Init() 会加载该微服务的所有配置,比如使用到的插件、设置的环境变量、命令行参数等,这些配置项会作为微服务的一部分来运行。可使用 s.Server().Options() 来获取这些配置。

我们在 Makefile 中设置了 GO_MICRO_BROKER 环境变量,go-micro 会使用该地址指定的 NATS 消息系统做事件的订阅和发布。

Publish 事件发布

当创建一个新用户时我们发布一个事件,完整代码见:GitHub

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// user-service/handler.go

const topic = "user.created"

type handler struct {
repo Repository
tokenService Authable
PubSub broker.Broker
}

func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error {
// 哈希处理用户输入的密码
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
req.Password = string(hashedPwd)
if err := h.repo.Create(req); err != nil {
return nil
}
resp.User = req

// 发布带有用户所有信息的消息
if err := h.publishEvent(req); err != nil {
return err
}
return nil
}

// 发送消息通知
func (h *handler) publishEvent(user *pb.User) error {
body, err := json.Marshal(user)
if err != nil {
return err
}

msg := &broker.Message{
Header: map[string]string{
"id": user.Id,
},
Body: body,
}

// 发布 user.created topic 消息
if err := h.PubSub.Publish(topic, msg); err != nil {
log.Fatalf("[pub] failed: %v\n", err)
}
return nil
}

...

在运行前请确保你的 Postgres 容器正常运行:

1
2
3
$ docker run -d -p 5432:5432 postgres
$ make build
$ make run

Subscribe 事件订阅

现在创建新的邮件服务:email-service,创建新用户时将通知它发邮件。

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
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
userPb "shippy/user-service/proto/user"
"github.com/micro/go-micro"
"log"
"github.com/micro/go-micro/broker"
_ "github.com/micro/go-plugins/broker/nats"
"encoding/json"
)

const topic = "user.created"

func main() {
srv := micro.NewService(
micro.Name("go.micro.srv.email"),
micro.Version("latest"),
)
srv.Init()

pubSub := srv.Server().Options().Broker
if err := pubSub.Connect(); err != nil {
log.Fatalf("broker connect error: %v\n", err)
}

// 订阅消息
_, err := pubSub.Subscribe(topic, func(pub broker.Publication) error {
var user *userPb.User
if err := json.Unmarshal(pub.Message().Body, &user); err != nil {
return err
}
log.Printf("[Create User]: %v\n", user)
go senEmail(user)
return nil
})

if err != nil {
log.Printf("sub error: %v\n", err)
}

if err := srv.Run(); err != nil {
log.Fatalf("srv run error: %v\n", err)
}
}

func senEmail(user *userPb.User) error {
log.Printf("[SENDING A EMAIL TO %s...]", user.Name)
return nil
}

在运行邮件服务之前确保 NATS 已运行:

1
$ docker run -d -p 4222:4222 nats

现在把 user-service 和 email-service 都运行起来,使用 user-cli 创建新用户 Ewan,能看到给用户 Ewan 发了一封邮件:

5.1

切换消息代理插件

值得一提的是基于 JSON 的 NATS 性能消耗会比 gRPC 更高一点,需要额外处理 JSON 字符串,只在少数应用场景十分适合。go-micro 也支持很多已被广泛使用的消息队列 / 发布订阅技术,参考:消息代理插件列表,因为 go-micro 做了抽象,在它们之间进行切换是十分容易的。比如你想将 nats 换为 googlepubsub:

1
2
3
4
5
6
7
// 修改容器的环境变量
// MICRO_BROKER=nats
MICRO_BROKER=googlepubsub

// 修改 user-service 导入的包
// _ "github.com/micro/go-plugins/broker/nats"
_"github.com/micro/go-plugins/broker/googlepubsub"

如果你不使用 go-micro,可使用 Go 实现的 NATS,事件发布:

1
2
3
4
nc, _ := nats.Connect(nats.DefaultURL)

// Simple Publisher
nc.Publish("user.created", userJsonString)

事件订阅:

1
2
3
4
5
// Simple Async Subscriber
nc.Subscribe("user.created", func(m *nats.Msg) {
user := convertUserString(m.Data)
go sendEmail(user)
})

使用如 NATS 的第三方消息代理插件,会让微服务失去使用 protobuf 进行二进制数据通信的优势(微服务之间不再直接调用),反而多了处理 JSON 数据的开销,不过 go-micro 对此早有对策。

Pubsub 层

go-micro 内置有 pubsub 层,其位于代理层的顶端,无需第三方 NATS 消息代理,恰如其分的使用了我们定义好的 protobuf,更新 user-service 使用 pubsub 代替 NATS:

1
2
3
4
5
6
7
8
// user-service/main.go

func main() {
...
publisher := micro.NewPublisher(topic, srv.Client())
pb.RegisterUserServiceHandler(srv.Server(), &service{repo, tokenService, publisher})
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// user-service/handler.go

func (h *handler) Create(ctx context.Context, req *pb.User, resp *pb.Response) error {
// 哈希处理用户输入的密码
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
req.Password = string(hashedPwd)
if err := h.repo.Create(req); err != nil {
return nil
}
resp.User = req

// 发布带有用户所有信息的消息
if err := h.Publisher.Publish(ctx, req); err != nil {
return err
}
return nil
}

更新邮件微服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// email-service/main.go

type Subscriber struct{}

func main() {
...
micro.RegisterSubscriber(topic, srv.Server(), new(Subscriber))
...
}

func (sub *Subscriber) Process(ctx context.Context, user *userPb.User) error {
log.Println("[Picked up a new message]")
log.Println("[Sending email to]:", user.Name)
return nil
}

现在我们微服务底层就使用了 User Protobuf 定义,并没有使用第三方消息代理。运行效果如下:

5.2

总结

本节先使用 go-micro 的 NATS 消息代理插件,使 user-service 在创建新用户时发布一个带有用户信息且 topic 为 “user.created” 的消息事件,订阅了此 topic 的 email-service 接收到消息后取出用户信息来发送邮件。之后使用 go-micro 自带的 pubsub 层代替了 NATS 充分发挥 protobuf 通信的优势。

下节我们将使用 React 编写微服务的管理界面,并研究一下 web 端如何与微服务交互。