Go 微服務:基於 RabbitMQ 和 AMQP 進行訊息傳遞

騰訊雲加社群發表於2019-03-03

歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~

本文來自雲+社群翻譯社,由Tnecesoc編譯。

介紹

微服務就是將應用程式的業務領域劃分為具有明確範圍的不同場景,並以分離的流程來執行這些場景,使得其中跨邊界的任何持久化的關係必須依賴最終的一致性,而不是 ACID 類事務或外來鍵約束。這些概念很多都來源於領域驅動設計(DDD),或受到了它的啟發。不過 DDD 是個要花一整個部落格系列來講的話題,這裡就先不提了。

在我們的 Go 微服務系列部落格還有微服務架構的背景下,實現服務間鬆耦合的一種方式是引入訊息傳遞機制來進行不需要遵循嚴格的請求 / 響應式的訊息交換或類似機制的服務的通訊。這裡要強調一點,引入訊息傳遞機制只是眾多可以用來實現服務間鬆耦合的策略之一。

正如我們在部落格系列的第 8 章看到的那樣,在 Spring Cloud 裡,Spring Cloud Config 伺服器將 RabbitMQ 作為了執行時的一個依賴專案,因此 RabbitMQ 應該是一個不錯的訊息中繼器(message broker)。

至於本系列部落格的這一章的內容,我們會在讀取特定的帳號物件的時候讓我們的 "account service" 往 RabbitMQ 的交換器裡面放一條訊息。這一訊息會被一個我們會在這篇部落格裡編寫的,全新的微服務所處理。我們同時也會將一些 Go 語言程式碼放在一個 “common” 庫裡面來實現在跨微服務情景下的程式碼複用。

記住第 1 部分的系統圖景嗎?下面是完成這一章的部分之後的樣子:

img

在完成這一部分之前我們要做很多工作,不過我們能夠做到。

原始碼

這部分會有很多新程式碼,我們不可能把它全放在部落格文章裡。若要取得完整的原始碼,不妨用 git clone 命令下載下來,並切換到第 9 章的分支:

git checkout P9
複製程式碼

傳送訊息

我們將實施一個簡單的模擬用例:當在 “account service” 中讀取某些 “VIP” 賬戶時,我們希望觸發 “VIP offer” 服務。這一服務在特定情況下會為賬戶持有者生成一個 “offer” 。在一個設計正確的領域模型裡面,accounts 物件還有 VIP offer 物件都是兩個獨立的領域,而它們應儘可能少地瞭解彼此。

img

這個 account service 應當從不直接訪問 VIP service 的儲存空間(即 offers)。在這種情況下,我們應該把一條訊息傳遞給 RabbitMQ 上的 “vip service”,並將業務邏輯和持久化儲存完全委任給 “vip service”。

我們將使用 AMQP 協議來進行所有通訊,AMQP 協議是一個作為 ISO 標準的應用層協議,其所實現的訊息傳遞能為系統帶來可互操作性。這裡我們就延用在第 8 章我們處理配置更新(configuration update)時候的設定,選用 streadway / amqp 這一 AMQP 庫。

讓我們重述一下 AMQP 中的交換器(exchange)與釋出者(publisher)、消費者(consumer)和佇列(queue)的關係:

img

釋出者會將一條訊息釋出到交換點,後者會根據一定的路由規則或登記了相應消費者的繫結資訊來講訊息的副本發到佇列(或分發到多個佇列)裡去。對此,Quora 上的這個回答有個很好的解釋。

與訊息傳遞有關的程式碼

由於我們想要使用新的以及現有的程式碼來從我們現有的 account service 和新的 vip service 裡面的 Spring Cloud 配置檔案裡面載入我們所需的配置,所以我們會在這裡建立我們的第一個共享庫。

首先在 /goblog 下建立新資料夾 common 來存放可重用的內容:

mkdir -p common/messaging
mkdir -p common/config
複製程式碼

我們將所有與 AMQP 相關的程式碼放在 messaging 資料夾裡面,並將配置檔案放在 config 資料夾裡。你也可以將 /goblog/accountservice/config 的內容複製到 /goblog/common/config 中 - 請記住,這要求我們更新先前從 account service 中匯入配置程式碼的 import 語句。不妨看看完整原始碼來查閱這部分的寫法。

跟訊息傳遞有關的程式碼會被封裝在一個檔案中,該檔案將定義應用程式用來連線,釋出和訂閱訊息的介面還有實際實現。實際上,我們用的 streadway / amqp 已經提供了很多實現 AMQP 訊息傳遞所需的模板程式碼,所以這部分的具體細節也便不深究了。

在 /goblog/common/messaging 中建立一個新的 *.*go 檔案:messagingclient.go

讓我們看看裡面主要應有什麼:

// 定義用來連線、釋出訊息、消費訊息的介面
type IMessagingClient interface {
        ConnectToBroker(connectionString string)
        Publish(msg []byte, exchangeName string, exchangeType string) error
        PublishOnQueue(msg []byte, queueName string) error
        Subscribe(exchangeName string, exchangeType string, consumerName string, handlerFunc func(amqp.Delivery)) error
        SubscribeToQueue(queueName string, consumerName string, handlerFunc func(amqp.Delivery)) error
        Close()
}
複製程式碼

上面這段程式碼定義了我們所用的訊息介面。這就是我們的 “account service” 和 “vip service” 在訊息傳遞時將要處理的問題,能通過抽象手段來消除系統的大部分複雜度。請注意,我選擇了兩個 “Produce” 和 “Consume” 的變體,以便與訂閱/釋出主題還有 direct / queue 訊息傳遞模式合在一起使用。

接下來,我們將定義一個結構體,該結構體將持有指向 amqp.Connection 的指標,並且我們將再加上一些必要的方法,以便它能(隱式地,Go 一直以來都是這樣)實現我們剛才宣告的介面。

// 介面實現,封裝了一個指向 amqp.Connection 的指標
type MessagingClient struct {
        conn *amqp.Connection
}
複製程式碼

介面的實現非常冗長,在此只給出其中兩個 - ConnectToBroker()PublishToQueue()

func (m *MessagingClient) ConnectToBroker(connectionString string) {
        if connectionString == "" {
                panic("Cannot initialize connection to broker, connectionString not set. Have you initialized?")
        }
        var err error
        m.conn, err = amqp.Dial(fmt.Sprintf("%s/", connectionString))
        if err != nil {
                panic("Failed to connect to AMQP compatible broker at: " + connectionString)
        }
}
複製程式碼

這就是我們獲得 connection 指標 (如 amqp.Dial) 的方法。如果我們丟掉了配置檔案,或者連線不上中繼器,那麼微服務就會丟擲一個 panic 異常,並會讓容器協調器重新建立一個新的例項。在這裡傳入的 connectionString 引數就如下例所示:

amqp://guest:guest@rabbitmq:5672/

注意,這裡的 rabbitmq broker 是以 service 這一 Docker Swarm 的模式下執行的。

PublishOnQueue() 函式很長 - 它跟官方提供的 streadway 樣例或多或少地有些不同,畢竟這裡簡化了它的一些引數。為了將訊息釋出到一個有名字的佇列,我們需要傳遞這些引數:

  • body - 以位元組陣列的形式存在。可以是 JSON,XML 或一些二進位制檔案。
  • queueName - 要傳送訊息的佇列的名稱。

若要了解交換器的更多詳情,那請參閱 RabbitMQ 文件

func (m *MessagingClient) PublishOnQueue(body []byte, queueName string) error {
        if m.conn == nil {
                panic("Tried to send message before connection was initialized. Don't do that.")
        }
        ch, err := m.conn.Channel()      // 從 connection 裡獲得一個 channel 物件
        defer ch.Close()
        // 提供一些引數宣告一個佇列,若相應的佇列不存在,那就建立一個
        queue, err := ch.QueueDeclare(
                queueName, // 佇列名
                false, // 是否持久存在
                false, // 是否在不用時就刪掉
                false, // 是否排外
                false, // 是否無等待
                nil, // 其他引數
        )
        // 往佇列釋出訊息
        err = ch.Publish(
                "", // 目標為預設的交換器
                queue.Name, // 路由關鍵字,例如佇列名
                false, // 必須釋出
                false, // 立即釋出
                amqp.Publishing{
                        ContentType: "application/json",
                        Body:        body, // JSON 正文, 以 byte[] 形式給出
                })
        fmt.Printf("A message was sent to queue %v: %v", queueName, body)
        return err
}
複製程式碼

這裡的模板程式碼略多,但應該不難理解。這段程式碼會宣告一個(如果不存在那就建立一個)佇列,然後把我們的訊息以位元組陣列的形式釋出給它。

將訊息釋出到一個有名字的交換器的程式碼會更復雜,因為它需要一段模板程式碼來宣告交換器,以及佇列,並把它們繫結在一起。這裡有一份完整的原始碼示例。

接下來,由於我們的 “MessageClient” 的實際使用者會是 /goblog/accountservice/service/handlers.go ,我們會往裡面再新增一個欄位,並在請求的帳戶有 ID “10000” 的時候往硬編碼程式序中的 “is VIP” 檢查方法中傳送一條訊息:

var DBClient dbclient.IBoltClient
var MessagingClient messaging.IMessagingClient     // NEW
func GetAccount(w http.ResponseWriter, r *http.Request) {
     ...
複製程式碼

然後:

    ...
    notifyVIP(account)   // 並行地傳送 VIP 訊息
    // 若有這樣的 account, 那就把它弄成一個 JSON, 然後附上首部和其他內容來打包
    data, _ := json.Marshal(account)
    writeJsonResponse(w, http.StatusOK, data)
}
// 如果這個 account 是我們硬編碼進來的 account, 那就開個協程來傳送訊息
func notifyVIP(account model.Account) {
        if account.Id == "10000" {
                go func(account model.Account) {
                        vipNotification := model.VipNotification{AccountId: account.Id, ReadAt: time.Now().UTC().String()}
                        data, _ := json.Marshal(vipNotification)
                        err := MessagingClient.PublishOnQueue(data, "vipQueue")
                        if err != nil {
                                fmt.Println(err.Error())
                        }
                }(account)
        }
}
複製程式碼

正好藉此機會展示一下呼叫一個新的協程(goroutine)時所使用的內聯匿名函式,即使用 go 關鍵字。我們不能因為要執行 HTTP 處理程式傳送訊息就把 “主” 協程阻塞起來,因此這也是增加一點並行性的好時機。

main.go 也需要有所更新,以便在啟動的時候能使用載入並注入到 Viper 裡面的配置資訊來初始化 AMQ 連線。

// 在 main 方法裡面呼叫這個函式
func initializeMessaging() {
if !viper.IsSet("amqp_server_url") {
panic("No 'amqp_server_url' set in configuration, cannot start")
}
service.MessagingClient = &messaging.MessagingClient{}
service.MessagingClient.ConnectToBroker(viper.GetString("amqp_server_url"))
service.MessagingClient.Subscribe(viper.GetString("config_event_bus"), "topic", appName, config.HandleRefreshEvent)
}
複製程式碼

這段沒什麼意思 - 我們通過建立一個空的訊息傳遞結構,並使用從 Viper 獲取的屬性值來呼叫 ConnectToBroker 來得到 service.MessagingClient 例項。如果我們的配置沒有 broker_url,那就拋一個 panic 異常,畢竟在不可能連線到中繼器的時候程式也沒辦法執行。

更新配置

我們在第 8 部分中已將 amqp_broker_url 屬性新增到了我們的 .yml 配置檔案裡面,所以這個步驟實際上已經做過了。

broker_url: amqp://guest:guest@192.168.99.100:5672 _(dev)_   
broker_url: amqp://guest:guest@rabbitmq:5672 _(test)_
複製程式碼

注意,我們在 “test” 配置檔案裡面填入的是 Swarm 服務名 “rabbitmq”,而不是從我的電腦上看到的 Swarm 的 LAN IP 地址。(大家的實際 IP 地址應該會有所不同,不過執行 Docker Toolbox 時 192.168.99.100 似乎是標準配置)。

我們並不推薦在配置檔案中填入使用者名稱和密碼的明文。在真實的使用環境中,我們通常可以使用在第 8 部分中看到的 Spring Cloud Config 伺服器裡面的內建加密功能。

單元測試

當然,我們至少應該編寫一個單元測試,以確保 handlers.go 中的 GetAccount 函式在有人請求由 “10000” 標識的非常特殊的帳戶時會嘗試去傳送一條訊息。

為此,我們需要在 handlers_test.go 中實現一個模擬的 IMessagingClient 還有一個新的測試用例。我們先從模擬開始。這裡我們將使用第三方工具 mockery 生成一個 IMessagingClient 介面的模擬實現(在 shell 執行下面的命令的時候一定要先把 GOPATH 設定好):

> go get github.com/vektra/mockery/.../
> cd $GOPATH/src/github.com/callistaenterprise/goblog/common/messaging 
> ./$GOPATH/bin/mockery -all -output .
  Generating mock for: IMessagingClient
複製程式碼

現在,在當前資料夾中就有了一個模擬實現檔案 IMessagingClient.go。我看這個檔案的名字不爽,也看不慣它的駝峰命名法,因此我們將它重新命名一下,讓它的名字能更明顯的表示它是一個模擬的實現,並且遵循本系列部落格的檔名的一貫風格:

 mv IMessagingClient.go mockmessagingclient.go
複製程式碼

我們可能需要在生成的檔案中稍微調整一下 import 語句,並刪除一些別名。除此之外,我們會對這個模擬實現採用一種黑盒方法 - 只假設它會在我們開始測試的時候起作用。

不妨也看一看這裡生成的模擬實現的原始碼,這跟我們在第 4 章中手動編寫的內容非常相似。

在 handlers_test.go 裡新增一個新的測試用例:

// 宣告一個模仿類來讓測試更有可讀性
var anyString = mock.AnythingOfType("string")
var anyByteArray = mock.AnythingOfType("[]uint8")  // == []byte
func TestNotificationIsSentForVIPAccount(t *testing.T) {
        // 配置 DBClient 的模擬實現
        mockRepo.On("QueryAccount", "10000").Return(model.Account{Id:"10000", Name:"Person_10000"}, nil)
        DBClient = mockRepo
        mockMessagingClient.On("PublishOnQueue", anyByteArray, anyString).Return(nil)
        MessagingClient = mockMessagingClient
        Convey("Given a HTTP req for a VIP account", t, func() {
                req := httptest.NewRequest("GET", "/accounts/10000", nil)
                resp := httptest.NewRecorder()
                Convey("When the request is handled by the Router", func() {
                        NewRouter().ServeHTTP(resp, req)
                        Convey("Then the response should be a 200 and the MessageClient should have been invoked", func() {
                                So(resp.Code, ShouldEqual, 200)
                                time.Sleep(time.Millisecond * 10)    // Sleep since the Assert below occurs in goroutine
                                So(mockMessagingClient.AssertNumberOfCalls(t, "PublishOnQueue", 1), ShouldBeTrue)
                        })
        })})
}
複製程式碼

有關的詳情都寫在了註釋裡。在此,我也看不慣在斷言 numberOfCalls 的後置狀態之前人為地搞個 10 ms 的睡眠,但由於模擬是在與 “主執行緒” 分離的協程中呼叫的,我們需要讓它稍微掛起一段時間等待主執行緒完成一些工作。在此也希望能對協程和管道(channel)有一個更好的慣用的單元測試方式。

我承認 - 使用這種測試方式的過程比在為 Java 應用程式編寫單元測試用例時使用 Mockito 更加冗長。不過,我還是認為它的可讀性不錯,寫起來也很簡單。

接著執行測試,並確保測試通過:

go test ./...
複製程式碼

執行

首先要執行 springcloud.sh 指令碼來更新配置伺服器。然後執行 copyall.sh 並等待幾秒鐘,來讓它完成對我們的 “account service” 的更新。然後我們再使用 curl 來獲取我們的 “特殊” 帳戶。

> curl http://$ManagerIP:6767/accounts/10000
{"id":"10000","name":"Person_0","servedBy":"10.255.0.11"}
複製程式碼

若順利的話,我們應該能夠開啟 RabbitMQ 的管理控制檯。然後再看看我們是否在名為 vipQueue 的佇列上收到了一條訊息:

open http://192.168.99.100:15672/#/queues
複製程式碼

img

在上圖的最底部,我們看到 “vipQueue” 有 1 條訊息。我們再呼叫一下 RabbitMQ 管理控制檯中的 “Get Message” 函式,然後我們應該可以看到這條訊息:

img

在 Go 上編寫消費者 - “vip service”

最後該從頭開始寫一個全新的微服務了,我們將用它來展示如何使用 RabbitMQ 的訊息。我們會將迄今為止在本系列中學到的東西用到裡面,其中包括:

  • HTTP 伺服器
  • 效能監控
  • 集中配置
  • 重用訊息傳遞機制程式碼

如果你執行過了 git checkout P9,那就應該可以在 root/goblog 資料夾中看到 “vipservice” 。

我不會在這裡介紹每一行程式碼,畢竟它有些部分跟 “accountservice” 有所重複。我們會將重點放在 我們剛剛所傳送的訊息的 “消費方式” 上。有幾點要注意:

  • 此時有兩個新的 .yml 檔案被新增到了 config-repo 裡面。它們是 vipservice-dev.yml 和 vipservice-test.yml*。*
  • copyall.sh 也有所更新,它會構建並部署 “accountservice” 和我們新編寫的 “vipservice”。

消費一條訊息

我們將使用 /goblog/common/messaging 和 SubscribeToQueue 函式中的程式碼,例如:

SubscribeToQueue(queueName string, consumerName string, handlerFunc func(amqp.Delivery)) error
複製程式碼

對此我們要提供這些引數:

  • 佇列名稱(例如 “vip_queue”)
  • 消費者名稱
  • 一個收到響應佇列的訊息時呼叫的回撥函式 - 就像我們在第 8 章中消費配置的更新那樣

將我們的回撥函式繫結到佇列的 SubscribeToQueue 函式的實現也沒什麼好說的。這是其原始碼,如果需要也可以看一看。

接下來我們快速瀏覽一下 vip service 的 main.go 來看看我們設定這些東西的過程:

var messagingClient messaging.IMessagingConsumer
func main() {
fmt.Println("Starting " + appName + "...")
config.LoadConfigurationFromBranch(viper.GetString("configServerUrl"), appName, viper.GetString("profile"), viper.GetString("configBranch"))
initializeMessaging()
// 確保在服務存在的時候關掉連線
handleSigterm(func() {
if messagingClient != nil {
messagingClient.Close()
}
})
service.StartWebServer(viper.GetString("server_port"))
}
// 在收到 "vipQueue" 發來的訊息時會呼叫的回撥函式
func onMessage(delivery amqp.Delivery) {
fmt.Printf("Got a message: %v\n", string(delivery.Body))
}
func initializeMessaging() {
        if !viper.IsSet("amqp_server_url") {
            panic("No 'broker_url' set in configuration, cannot start")
        }
        messagingClient = &messaging.MessagingClient{}
        messagingClient.ConnectToBroker(viper.GetString("amqp_server_url"))
        // Call the subscribe method with queue name and callback function
        err := messagingClient.SubscribeToQueue("vip_queue", appName, onMessage)
        failOnError(err, "Could not start subscribe to vip_queue")
        err = messagingClient.Subscribe(viper.GetString("config_event_bus"), "topic", appName, config.HandleRefreshEvent)
        failOnError(err, "Could not start subscribe to " + viper.GetString("config_event_bus") + " topic")
}
複製程式碼

很熟悉對吧?我們在後續的章節也很可能會反覆提到設定並啟動我們加進去的微服務的方法。這也是基礎知識的一部分。

這個 onMessage 函式只記錄了我們收到的任何 “VIP” 訊息的正文。如果我們要實現更多的模擬用例,就得引入一些花哨的邏輯來確定賬戶持有者是否有資格獲得 “super-awesome buy all our stuff (tm)” 的待遇,並且也可能要往 “VIP offer 資料庫“ 裡寫入記錄。有興趣的話不妨也實施一下這一邏輯,然後交個 pull request。

最後再提一下這段程式碼。在這段程式碼的幫助下,我們可以按下 Ctrl + C 來殺掉一個服務的例項,或者我們也可以等待 Docker Swarm 來殺死一個服務例項。

   func handleSigterm(handleExit func()) {
           c := make(chan os.Signal, 1)
           signal.Notify(c, os.Interrupt)
           signal.Notify(c, syscall.SIGTERM)
           go func() {
                   <-c
                   handleExit()
                   os.Exit(1)
           }()
   }
複製程式碼

這段程式碼在可讀性上並不比別的程式碼好,它所做的只是將管道 “c” 註冊為 os.Interruptsyscall.SIGTERM 的監聽器,並且會阻塞性地監聽 “c” 上的訊息,直到接到任意訊號為止。這使我們能夠確信,只要微服務的例項被殺了,這裡的 handleExit() 函式就會被呼叫。若還是不能確信的話,可以用 Ctrl + C 或者 Docker Swarm scaling 來測試一下。kill 指令也可以,不過 kill -9 就不行。因此,除非必須,最好不要用 kill -9 來結束任何東西的執行。

handleExit() 函式將呼叫我們在 IMessageConsumer 介面上宣告的 Close() 函式,該函式會確保 AMQP 連線的正常關閉。

部署並執行

這裡的 copyall.sh 指令碼已經更新過了。若有跟從上面的步驟,並且確保了合 Github 的 P9 分支的一致性,那就可以執行了。在完成部署之後,執行 docker service ls 就應該會列印這樣的內容:

> docker service ls
ID            NAME            REPLICAS  IMAGE                        
kpb1j3mus3tn  accountservice  1/1       someprefix/accountservice                                                                            
n9xr7wm86do1  configserver    1/1       someprefix/configserver                                                                              
r6bhneq2u89c  rabbitmq        1/1       someprefix/rabbitmq                                                                                  
sy4t9cbf4upl  vipservice      1/1       someprefix/vipservice                                                                                
u1qcvxm2iqlr  viz             1/1       manomarks/visualizer:latest
複製程式碼

或者也可以使用 dvizz Docker Swarm 服務渲染器檢視狀態:

img

檢查日誌

由於 docker service logs 功能在 1.13.0 版本里面被標記成了實驗性功能,因此我們必須用老一套的方式來檢視 “vipservice” 的日誌。

首先,執行 docker ps 找出 CONTAINER ID:

> docker ps
CONTAINER ID        IMAGE                                                                                       
a39e6eca83b3        someprefix/vipservice:latest           
b66584ae73ba        someprefix/accountservice:latest        
d0074e1553c7        someprefix/configserver:latest
複製程式碼

記下 vipservice 的 CONTAINER ID,並執行 docker logs -f 檢查其日誌:

> docker logs -f a39e6eca83b3
Starting vipservice...
2017/06/06 19:27:22 Declaring Queue ()
2017/06/06 19:27:22 declared Exchange, declaring Queue ()
2017/06/06 19:27:22 declared Queue (0 messages, 0 consumers), binding to Exchange (key 'springCloudBus')
Starting HTTP service at 6868
複製程式碼

開啟另一個命令列視窗,並 curl 一下我們的特殊賬戶物件。

> curl http://$ManagerIP:6767/accounts/10000
複製程式碼

如果一切正常,我們應該在原始視窗的日誌中看到響應佇列的訊息。

Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:27.033757223 +0000 UTC"}
複製程式碼

工作佇列

用於跨服務例項分發工作的模式利用了工作佇列的概念。每個 “vip 訊息” 應該由一個“vipservice”例項處理。

img

所以讓我們看看使用 docker service scale 命令將 “vipservice” 擴充套件成兩個例項時的情形:

> docker service scale vipservice=2
複製程式碼

新的 “vipservice” 例項應該能在幾秒鐘內完成部署。

由於我們在 AMQP 中使用了 direct / queue 的傳送方法,我們應該會見到一種輪轉排程式(round-robin)的分發情形。再用 curl 觸發四個 VIP 帳戶的查詢:

> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000
> curl http://$ManagerIP:6767/accounts/10000
複製程式碼

再次檢查我們原始的 “vipservice” 的日誌:

 > docker logs -f a39e6eca83b3
Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:27.033757223 +0000 UTC"}
Got a message: {"accountId":"10000","readAt":"2017-02-15 20:06:29.073682324 +0000 UTC"}
複製程式碼

正如我們所預期的那樣,我們看到第一個例項處理了四個訊息中的兩個。如果我們對另一個 “vipservice” 例項也執行一次 docker logs,那麼我們也應該會在那裡看到兩條訊息。

測試消費者

實際上,我並沒有真正想出一個好的方式來在避免花費大量的時間模擬一個 AMQP 庫的前提下,對 AMQP 消費者進行單元測試。在 messagingclient_test.go 裡面準備了一個測試,用於測試訂閱者對傳入訊息的等待以及處理的一個迴圈。不過並沒有值得一提的地方。

為了更全面地測試訊息傳遞機制,我可能會在後續的部落格文章中回顧關於整合測試的話題。使用 Docker Remote API 或 Docker Compose 進行 go 測試。測試將啟動一些服務,比如能讓我們在測試程式碼裡面傳送還有接收訊息的 RabbitMQ。

足跡和效能

我們這次不弄效能測試。在傳送和接收一些訊息之後快速瀏覽一下記憶體使用情況就足夠了:

CONTAINER                                    CPU %               MEM USAGE / LIMIT
   vipservice.1.tt47bgnmhef82ajyd9s5hvzs1       0.00%               1.859MiB / 1.955GiB
   accountservice.1.w3l6okdqbqnqz62tg618szsoj   0.00%               3.434MiB / 1.955GiB
   rabbitmq.1.i2ixydimyleow0yivaw39xbom         0.51%               129.9MiB / 1.955GiB
複製程式碼

上面便是在處理了幾個請求之後的記憶體使用情況。新的 “vipservice” 並不像 “accountservice” 那麼複雜,因此在啟動後它應該會使用更少的記憶體。

概要

這大概是這個系列中最長的一部分了!我們在這章完成了這些內容:

  • 更深入地考察了 RabbitMQ 和 AMQP 協議。
  • 增加了全新的 “vipservice”。
  • 將與訊息傳遞(和配置)有關的程式碼提取到了可重用的子專案中。
  • 基於 AMQP 協議釋出 / 訂閱訊息。
  • 用 mockery 生成模擬程式碼。

問答

微服務架構:跨服務資料共享如何實現?

相關閱讀

在微服務之間進行通訊

RabbitMQ與AMQP協議

使用Akka HTTP構建微服務:CDC方法


此文已由作者授權騰訊雲+社群釋出,原文連結:https://cloud.tencent.com/developer/article/1149121?fromSource=waitui

歡迎大家前往騰訊雲+社群或關注雲加社群微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~

相關文章