Micro In Action:Pub/Sub

本文是Micro[1]系列文章的第三篇。我們將以實際開發微服務爲主線,順帶解析相關功能。從最基本的話題開始,逐步轉到高級特性。

接下來談談異步消息處理。要構建一個可伸縮、高容錯、高併發的系統, 異步消息處理是一個關鍵技術。這種技術雖然強大, 但開發起來也相當麻煩, 遠沒有同步請求那樣簡單直接。

好在 Micro 對這個編程模型作了非常好的抽象與封裝,供我們便利地使用。

除此之外, 藉助 Micro 的接口抽象, 我們可以透明(或者說幾乎透明)地支持各種消息服務器。Micro 默認提供了基於 HTTP 的消息服務器實現。同時也以插件形式提供了多種主流消息服務系統的支持。包括 Kafka,RabbitMQ,Nats,MQTT,NSQ,Amazon SQS 等。你可以到插件主頁[2]瞭解更多詳細說明。 這使得我們在因業務需要而切換消息服務時,可以幾乎不修改任何業務代碼。

Micro 支持以兩種不同方式處理異步消息, 一種是Pub/Sub[3],另一種是使用micro.Broker接口進行消息收發。 前者相對簡單,後者則能提供更大靈活度。

Micro 內置的 Pub/Sub 功能統一併簡化了異步消息的收、發、編碼和解碼。這把開發者從底層技術細節中解放出來,去專注於創造業務價值。多數情況下我們應優先選擇此方式。

下面我們將以實例解析一套 Pub/Sub 系統的開發和運行。

Sub,訂閱消息

在本系列第一篇文章[4]中, 我們創建了一個示例項目,其中已經包含了訂閱相關的代碼。
首先定義消息處理 Handler, ./subscriber/hello.go 代碼如下:

package subscriberimport (
   "context"
   "github.com/micro/go-micro/util/log"   hello "hello/proto/hello"
)type Hello struct{}func (e *Hello) Handle(ctx context.Context, msg *hello.Message) error {
   log.Log("Handler Received message: ", msg.Say)
   return nil
}func Handler(ctx context.Context, msg *hello.Message) error {
   log.Log("Function Received message: ", msg.Say)
   return nil
}

接收消息的代碼可以是一個函數,也可以是對象的方法, 其簽名爲 func(context.Context, v interface{}) error。

注意在示例中方法的第二個參數是 *hello.Message, 此類型在.proto 文件中定義。Micro 框架會自動完成消息的解碼。我們在 Handler 中可以直接使用。

準備好消息 Handler 以後, 需要進行註冊, ./main.go 相關代碼如下:

...
// Register Struct as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), new(subscriber.Hello))// Register Function as Subscriber
micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler)
...

上述代碼分別將對象和函數註冊爲消息處理 Handler, 接收 “com.foo.srv.hello” 這個主題(Topic )下的消息。

如果想更詳細地控制訂閱策略, 需要爲micro.RegisterSubscriber方法傳遞更多參數。 我們先們看一下此方法的簽名:

func RegisterSubscriber(topic string, s server.Server, h interface{}, opts ...server.SubscriberOption) error

第一個參數代表訂閱的 Topic。 第二個參數是server.Server,其實例可從 service 中取得。 第三個參數是消息處理 Handler。

最後一個可選參數, 用於控制訂閱行爲,它的類型是server.SubscriberOption。目前 Micro 內置提供有 4 個選項:

  1. server.DisableAutoAck() SubscriberOption , 禁用自動確認。
  2. server.SubscriberContext(ctx context.Context) SubscriberOption, 指定訂閱 Context。
  3. server.InternalSubscriber(b bool) SubscriberOption,內部訂閱, 不把此訂閱者信息廣播到註冊中心。
  4. server.SubscriberQueue(n string) SubscriberOption,指定隊列名。

注:個人認爲框架暴露出來的選項還是太少了。 如果有稍高要求,就不得不去使用 Broker 接口。例如控制消息的持久化,控制出錯重發的策略這些都是比較常見的需求。希望在後續版本中這一點可以得到擴展。

在上述幾個選項中,server.SubscriberQueue 值得單獨說明一下。

我們知道在 Pub/Sub 模型中有 Queue (或 Channel) 的概念, 如果一個 Topic 的多個訂閱者各自擁有自己的 Queue, 那麼消息會被複制分發到不同 Queue 中, 使得每個訂閱者都可以接收到全部消息。

Micro 默認會爲每個訂閱者實例創建一個全局唯一 Queue。如果想要不同訂閱者或單一訂閱者的多個實例共享一個隊列, 這時就需要用server.SubscriberQueue來明確指定隊列名稱了:

micro.RegisterSubscriber("com.foo.srv.hello", service.Server(), subscriber.Handler, server.SubscriberQueue("foo_bar"))

這樣當有多個訂閱者節點運行時, 大家就會共享一個 Queue。因此消息會被分發到某一個節點進行處理, 避免相同的消息被重複處理。考慮到在分佈系統中單一服務多節點運行是很常見的場景, 所以我的建議是: 除非你知道自己在作什麼, 否則永遠明確指定隊列 — — 哪怕目前只有一個訂閱實例。 最常見的作法是讓隊列與 Topic 同名。

至此, Pub/Sub 模型中的 Sub 部分就準備好了,下面開始編寫 Pub 代碼。

Pub,發佈消息

我們創建一個發佈消息的項目, 其結構如下:

.
├── main.go
├── plugin.go
├── proto/hello
│   └── hello.proto
│   └── hello.pb.go
│   └── hello.pb.micro.go
├── go.mod
├── go.sum

其中除了main.go 內容有所不同以外,其它文件的內容與含義均與《Micro In Action(二)》[5]所述一致,此處不再贅述。

main.go 文件代碼如下:

package main

import (
	"context"
	"log"
	"time"

	"github.com/micro/go-micro"

	hello "hello/proto/hello"
)

func main() {
	// New Service
	service := micro.NewService(
		micro.Name("com.foo.srv.hello.pub"), // name the client service
	)
	// Initialise service
	service.Init()

	// create publisher
	pub := micro.NewPublisher("com.foo.srv.hello", service.Client())

	// publish message every second
	for now := range time.Tick(time.Second) {
		if err := pub.Publish(context.TODO(), &hello.Message{Say: now.String()}); err != nil {
			log.Fatal("publish err", err)
		}
	}
}
  • 首先創建並初始化micro.Service實例, 將其命名爲com.foo.srv.hello.pub。這個名字並沒有特殊意義,在真實項目中很可能完全不同。
  • 然後指定發送消息的目標 Topic,創建micro.Publisher實例。
  • 接下來每秒鐘送一條消息,消息類型爲*hello.Message, 框架會自動對消息進行編碼。

與訂閱功能類似, 發佈接口也支持可選選項,此選項可以用來控制發送行爲。Publisher接口的定義如下:

// Publisher is syntactic sugar for publishing
type Publisher interface {
   Publish(ctx context.Context, msg interface{}, opts ...client.PublishOption) error
}

目前 Micro 框架僅提供了一個內置的發佈選項:

  • client.WithExchange(e string) PublishOption,它用於控制消息的 Exchange(此概念將在後續文章中展開說明)。

運行

在 pub 項目準備以後,首先運行 hello server,然後運行 pub 項目。

之後我們將在 hello server 的控制檯中看到每秒追加的接收消息日誌:

$ go run main.go plugin.go2020-02-14 14:18:24.368336 I | Transport [http] Listening on [::]:56970
2020-02-14 14:18:24.368429 I | Broker [http] Connected to [::]:56971
2020-02-14 14:18:24.368680 I | Registry [mdns] Registering node: com.foo.srv.hello-14b7ea99-167f-4136-ad11-ae22d45ed302
2020-02-14 14:18:24.370575 I | Subscribing com.foo.srv.hello-14b7ea99-167f-4136-ad11-ae22d45ed302 to topic: com.foo.srv.hello
2020-02-14 14:18:24.370784 I | Subscribing com.foo.srv.hello-14b7ea99-167f-4136-ad11-ae22d45ed302 to topic: com.foo.srv.hello
2020-02-14 14:18:40.415610 I | Handler Received message: 2020-02-14 14:18:40.309255 +0800 CST m=+1.007480205
2020-02-14 14:18:40.415651 I | Function Received message: 2020-02-14 14:18:40.309255 +0800 CST m=+1.007480205
2020-02-14 14:18:41.310969 I | Handler Received message: 2020-02-14 14:18:41.310352 +0800 CST m=+2.008611968
2020-02-14 14:18:41.310999 I | Function Received message: 2020-02-14 14:18:41.310352 +0800 CST m=+2.008611968
...

總結

Micro 對異步消息支持很完備。 既支持高層次的 Pub/Sub 模式, 也支持面向 Broker 接口的底層收發操作。

其中 Pub/Sub 功能極大地簡化了異步消息系統的開發, 使得我們可以忽略技術細節更聚焦在業務之上。

開發者只需定義好發送方,接收方以及消息內容, 其它工作全部由框架完成。 再不用考慮異步消息系統中常見的問題, 比如消息的路由、重發、接收確認, 也不用考慮消息內容的編碼與解碼。

當然這個簡化也帶來了一些侷限, 如果 Pub/Sub 不能滿足你的需求, 那麼請關注本系列下篇文章:Message Broker。

參考資料

[1] Micro: https://micro.mu/
[2] 插件主頁: https://github.com/micro/go-plugins/tree/master/broker
[3] Pub/Sub: https://en.wikipedia.org/wiki/Publish–subscribe_pattern
[4] 第一篇文章: https://studygolang.com/articles/27111
[5]《Micro In Action(二)》: https://studygolang.com/articles/27173
[6] https://mp.weixin.qq.com/s/r7XfwNSfGkhd0e4G8nxvWw

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章