分散式日誌儲存架構程式碼實踐

7small7發表於2022-05-08

上一篇,我們針對分散式日誌儲存方案設計做了一個理論上的分析與總結,文章地址。本文我們將結合其中的一種方案進行實戰程式碼的演示。另外一種方案,將在下一篇文章進行分享,此篇文章分享的是MongoDB架構模式。在知乎上釋出該文章時,有人提到使用opentelemtry+tsdb,感興趣的可以去了解一下。

架構模式

通過上一篇的分析,我們大致總結出這樣的一個架構設計,架構圖如下:

  1. 業務A、業務B、業務C和業務D表示我們實際的介面地址。當客戶端傳送請求時,直接的處理模組。系統日誌的生成也是在該模組中進行生成。
  2. MQ服務,則是作為日誌佇列,臨時儲存日誌訊息。這樣是為了提高日誌的處理能力。在高併發的業務場景中,如果實時的將日誌寫入到MongoDB中,這樣難免會降低業務處理的速度。
  3. MongoDB服務,則是最終的日誌落地。也就是說將我們的日誌儲存到磁碟,以達到資料的持久化,避免資料丟失。
  4. 對於系統的日誌檢視,我們可以直接登入MongoDB服務進行SQL查詢。一般為了效率、安全等原因,會提供一個管理介面來實時檢視MongoDB的日誌。這裡就是我們的web展示介面。可以通過web介面對日誌做查詢、篩選、刪除等操作。

上面提到的是一個架構的大致流程圖。下面將具體的程式碼演示,需要檢視程式碼的可以通過Github倉庫地址獲取。

程式碼演示

程式碼中要操作RabbitMQ服務、MongoDB服務、API業務邏輯處理和其他的服務,我這裡將程式碼呼叫邏輯設計為如下結構。

magin.go(入口檔案)->api(業務處理)->rabbitmq(日誌生產者、消費者)->MongoDB(日誌持久化)。
整理程式碼架構如下:

程式碼說明

下面羅列幾個使用到的技術棧以及對應的版本,可能需要在使用本程式碼時,需要注意一下這些服務的版本相容,避免程式碼無法執行。

  1. Go version 1.16。
  2. RabbitMQ version 3.10.0。
  3. 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環境,以便我們後期做實際程式碼演示。

相關文章