13

译文链接:wuYin/blog
原文链接: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 插件集成到我们的代码中:

// 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

// 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 容器正常运行:

$ docker run -d -p 5432:5432 postgres
$ make build
$ make run

Subscribe 事件订阅

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

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 已运行:

$ 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:

// 修改容器的环境变量
// 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,事件发布:

nc, _ := nats.Connect(nats.DefaultURL)

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

事件订阅:

// 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:

// user-service/main.go

func main() {
    ... 
    publisher := micro.NewPublisher(topic, srv.Client())
    pb.RegisterUserServiceHandler(srv.Server(), &service{repo, tokenService, publisher})
    ... 
}
// 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
}

更新邮件微服务:

// 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 端如何与微服务交互。


wuYin
2.2k 声望506 粉丝

About: github.com/wuYin