本文是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 個選項:
- server.DisableAutoAck() SubscriberOption , 禁用自動確認。
- server.SubscriberContext(ctx context.Context) SubscriberOption, 指定訂閱 Context。
- server.InternalSubscriber(b bool) SubscriberOption,內部訂閱, 不把此訂閱者信息廣播到註冊中心。
- 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