上一篇,我們針對分散式日誌儲存方案設計做了一個理論上的分析與總結,文章地址。本文我們將結合其中的一種方案進行實戰程式碼的演示。另外一種方案,將在下一篇文章進行分享,此篇文章分享的是MongoDB架構模式。在知乎上釋出該文章時,有人提到使用opentelemtry+tsdb,感興趣的可以去了解一下。
架構模式
通過上一篇的分析,我們大致總結出這樣的一個架構設計,架構圖如下:
- 業務A、業務B、業務C和業務D表示我們實際的介面地址。當客戶端傳送請求時,直接的處理模組。系統日誌的生成也是在該模組中進行生成。
- MQ服務,則是作為日誌佇列,臨時儲存日誌訊息。這樣是為了提高日誌的處理能力。在高併發的業務場景中,如果實時的將日誌寫入到MongoDB中,這樣難免會降低業務處理的速度。
- MongoDB服務,則是最終的日誌落地。也就是說將我們的日誌儲存到磁碟,以達到資料的持久化,避免資料丟失。
- 對於系統的日誌檢視,我們可以直接登入MongoDB服務進行SQL查詢。一般為了效率、安全等原因,會提供一個管理介面來實時檢視MongoDB的日誌。這裡就是我們的web展示介面。可以通過web介面對日誌做查詢、篩選、刪除等操作。
上面提到的是一個架構的大致流程圖。下面將具體的程式碼演示,需要檢視程式碼的可以通過Github倉庫地址獲取。
程式碼演示
程式碼中要操作RabbitMQ服務、MongoDB服務、API業務邏輯處理和其他的服務,我這裡將程式碼呼叫邏輯設計為如下結構。
magin.go(入口檔案)->api(業務處理)->rabbitmq(日誌生產者、消費者)->MongoDB(日誌持久化)。
整理程式碼架構如下:
程式碼說明
下面羅列幾個使用到的技術棧以及對應的版本,可能需要在使用本程式碼時,需要注意一下這些服務的版本相容,避免程式碼無法執行。
- Go version 1.16。
- RabbitMQ version 3.10.0。
- MongoDB version v5.0.7。
下面對幾個稍微重要的程式碼段,進行簡單說明,完整程式碼直接檢視Github倉庫即可。
入口檔案
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"gologs/api"
)
func main() {
r := gin.Default()
// 定義一個order-api的路由地址,並做對應的介面返回
r.GET("/order", func(ctx *gin.Context) {
orderApi, err := api.OrderApi()
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"code": 1,
"msg": orderApi,
"data": map[string]interface{}{},
})
}
ctx.JSON(http.StatusOK, gin.H{
"code": 1,
"msg": orderApi,
"data": map[string]interface{}{},
})
})
// 指定服務地址和埠號
err := r.Run(":8081")
if err != nil {
fmt.Println("gin server fail, fail reason is ", err)
}
}
訂單業務邏輯
package api
import (
"time"
"gologs/rabbit"
)
// 訂單業務邏輯處理,並呼叫Rabbit服務投遞order日誌
func OrderApi() (string, error) {
orderMsg := make(map[string]interface{})
orderMsg["time"] = time.Now()
orderMsg["type"] = "order"
err := rabbit.SendMessage(orderMsg)
if err != nil {
return "write rabbitmq log fail", err
}
return "", nil
}
RabbitMQ處理日誌
package rabbit
import (
"encoding/json"
"github.com/streadway/amqp"
"gologs/com"
)
func SendMessage(msg map[string]interface{}) error {
channel := Connection()
declare, err := channel.QueueDeclare("logs", false, false, false, false, nil)
if err != nil {
com.FailOnError(err, "RabbitMQ declare queue fail!")
return err
}
marshal, err := json.Marshal(msg)
if err != nil {
return err
}
err = channel.Publish(
"",
declare.Name,
false,
false,
amqp.Publishing{
ContentType: "text/plain", // message type
Body: marshal, // message body
DeliveryMode: amqp.Persistent,
})
if err != nil {
com.FailOnError(err, "rabbitmq send message fail!")
return err
}
return nil
}
消費者消費訊息
package rabbit
import (
"encoding/json"
"fmt"
"time"
"gologs/com"
"gologs/mongo"
)
func ConsumerMessage() {
channel := Connection()
declare, err := channel.QueueDeclare("logs", false, false, false, false, nil)
if err != nil {
com.FailOnError(err, "queue declare fail")
}
consume, err := channel.Consume(
declare.Name,
"",
true,
false,
false,
false,
nil,
)
if err != nil {
com.FailOnError(err, "message consumer failt")
}
for d := range consume {
msg := make(map[string]interface{})
err := json.Unmarshal(d.Body, &msg)
fmt.Println(msg)
if err != nil {
com.FailOnError(err, "json parse error")
}
one, err := mongo.InsertOne(msg["type"].(string), msg)
if err != nil {
com.FailOnError(err, "mongodb insert fail")
}
fmt.Println(one)
time.Sleep(time.Second * 10)
}
}
呼叫MongoDB持久化日誌
package mongo
import (
"context"
"errors"
"gologs/com"
)
func InsertOne(collectionName string, logs map[string]interface{}) (interface{}, error) {
collection := Connection().Database("logs").Collection(collectionName)
one, err := collection.InsertOne(context.TODO(), logs)
if err != nil {
com.FailOnError(err, "write mongodb log fail")
return "", errors.New(err.Error())
}
return one.InsertedID, nil
}
實戰演示
上面大致分享了程式碼邏輯,接下來演示程式碼的執行效果。
啟動服務
啟動服務,需要進入到log是目錄下面,main.go
就是實際的入口檔案。
啟動日誌消費者
啟動日誌消費者,保證一旦有日誌,消費者能把日誌實時儲存到MongoDB中。同樣的需要到logs目錄下執行該命令。
go run rabbit_consumer.go
呼叫API服務
為了演示,這裡直接使用瀏覽器去訪問該order對應的介面地址。http://127.0.0.1:8081/order
。介面返回如下資訊:
如果code是1則表示介面成功,反之是不成功,需要在呼叫的時候注意一下。
這裡可以多訪問幾次,檢視RabbitMQ中的佇列資訊。如果消費者消費的比較慢,應該可以看到如下資訊:
消費者監控
由於我們在啟動服務時,就單獨開啟了一個消費者執行緒,這個執行緒正常情況下時一直作為後臺程式在執行。我們可以檢視大致的消費資料內容,如下圖:
MongoDB檢視資料
RabbitMQ消費者將日誌資訊儲存到MongoDB中,接下來直接通過MongoDB進行查詢。
db.order.find();
[
{
"_id": {"$oid": "627675df5f796f95ddb9bbf4"},
"time": "2022-05-07T21:36:02.374928+08:00",
"type": "order"
},
{
"_id": {"$oid": "627675e95f796f95ddb9bbf6"},
"time": "2022-05-07T21:36:02.576065+08:00",
"type": "order"
}
................
]
文末總結
對於該架構的總體演示,就到此結束。當然還有很多細節需要完善,此篇內容主要是分享一個大致的流程。下一篇我們將分享如何在Linux上大家ELK環境,以便我們後期做實際程式碼演示。