分散式任務 + 訊息佇列框架 go-queue

kevwan 發表於 2021-03-21
Go
  1. 為什麼寫這個庫
  2. 應用場景有哪些
  3. 如何使用
  4. 總結

為什麼要寫這個庫?

在開始自研 go-queue 之前,針對以下我們調研目前的開源佇列方案:

beanstalkd

beanstalkd 有一些特殊好用功能:支援任務 priority、延時 (delay)、超時重發 (time-to-run) 和預留 (buried),能夠很好的支援分散式的後臺任務和定時任務處理。如下是 beanstalkd 基本部分:

  • job:任務單元;
  • tube:任務佇列,儲存統一型別 job。producer 和 consumer 操作物件;
  • producerjob 生產者,通過 put 將 job 加入一個 tube;
  • consumerjob 消費者,通過 reserve/release/bury/delete 來獲取 job 或改變 job 的狀態;

很幸運的是官方提供了 go client:https://github.com/beanstalkd/go-beanstalk

但是這對不熟悉 beanstalkd 操作的 go 開發者而言,需要學習成本。

kafka

類似基於 kafka 訊息佇列作為儲存的方案,儲存單元是訊息,如果要實現延時執行,可以想到的方案是以延時執行的時間作為 topic,這樣在大型的訊息系統中,充斥大量一次性的 topicdq_1616324404788, dq_1616324417622),當時間分散,會容易造成磁碟隨機寫的情況。

而且在 go 生態中,

同時考慮以下因素:

  • 支援延時任務
  • 高可用,保證資料不丟失
  • 可擴充套件資源和效能

所以我們自己基於以上兩個基礎元件開發了 go-queue

  1. 基於 beanstalkd 開發了 dq,支援定時和延時操作。同時加入 redis 保證消費唯一性。
  2. 基於 kafka 開發了 kq,簡化生產者和消費者的開發 API,同時在寫入 kafka 使用批量寫,節省 IO。

整體設計如下:

分散式任務 + 訊息佇列框架 go-queue

應用場景

首先在消費場景來說,一個是針對任務佇列,一個是訊息佇列。而兩者最大的區別:

  • 任務是沒有順序約束;訊息需要;
  • 任務在加入中,或者是等待中,可能存在狀態更新(或是取消);訊息則是單一的儲存即可;

所以在背後的基礎設施選型上,也是基於這種消費場景。

  • dq:依賴於beanstalkd ,適合延時、定時任務執行;
  • kq:依賴於 kafka ,適用於非同步、批量任務執行;

而從其中 dq 的 API 中也可以看出:

// 延遲任務執行
- dq.Delay(msg, delayTime);

// 定時任務執行
- dq.At(msg, atTime);

而在我們內部:

  • 如果是 非同步訊息消費/推送 ,則會選擇使用 kqkq.Push(msg)
  • 如果是 15 分鐘提醒/ 明天中午傳送簡訊 等,則使用 dq

如何使用

分別介紹 dqkq 的使用方式:

dq

// [Producer]
producer := dq.NewProducer([]dq.Beanstalk{
    {
        Endpoint: "localhost:11300",
        Tube:     "tube",
    },
    {
        Endpoint: "localhost:11301",
        Tube:     "tube",
    },
})  

for i := 1000; i < 1005; i++ {
    _, err := producer.Delay([]byte(strconv.Itoa(i)), time.Second*5)
    if err != nil {
        fmt.Println(err)
    }
}
// [Consumer]
consumer := dq.NewConsumer(dq.DqConf{
  Beanstalks: []dq.Beanstalk{
    {
      Endpoint: "localhost:11300",
      Tube:     "tube",
    },
    {
      Endpoint: "localhost:11301",
      Tube:     "tube",
    },
  },
  Redis: redis.RedisConf{
    Host: "localhost:6379",
    Type: redis.NodeType,
  },
})
consumer.Consume(func(body []byte) {
  // your consume logic
  fmt.Println(string(body))
})

和普通的 生產者 - 消費者 模型類似,開發者也只需要關注以下:

  1. 開發者只需要關注自己的任務型別「延時/定時」
  2. 消費端的消費邏輯

kq

producer.go

// message structure
type message struct {
    Key     string `json:"key"`
    Value   string `json:"value"`
    Payload string `json:"message"`
}

pusher := kq.NewPusher([]string{
    "127.0.0.1:19092",
    "127.0.0.1:19093",
    "127.0.0.1:19094",
}, "kq")

ticker := time.NewTicker(time.Millisecond)
for round := 0; round < 3; round++ {
    select {
    case <-ticker.C:
        count := rand.Intn(100)
    // 準備訊息
        m := message{
            Key:     strconv.FormatInt(time.Now().UnixNano(), 10),
            Value:   fmt.Sprintf("%d,%d", round, count),
            Payload: fmt.Sprintf("%d,%d", round, count),
        }
        body, err := json.Marshal(m)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println(string(body))
    // push to kafka broker
        if err := pusher.Push(string(body)); err != nil {
            log.Fatal(err)
        }
    }
}

config.yaml

Name: kq
Brokers:
  - 127.0.0.1:19092
  - 127.0.0.1:19092
  - 127.0.0.1:19092
Group: adhoc
Topic: kq
Offset: first
Consumers: 1

consumer.go

var c kq.KqConf
conf.MustLoad("config.yaml", &c)

// WithHandle: 具體的處理msg的logic
// 這也是開發者需要根據自己的業務定製化
q := kq.MustNewQueue(c, kq.WithHandle(func(k, v string) error {
  fmt.Printf("=> %s\n", v)
  return nil
}))
defer q.Stop()
q.Start()

dq 不同的是:開發者不需要關注任務型別(在這裡也沒有任務的概念,傳遞的都是 message data)。

其他操作和 dq 類似,只是將 業務處理函式 當成配置直接傳入消費者中。

總結

在我們目前的場景中,kq 大量使用在我們的非同步訊息服務;而延時任務,我們除了 dq,還可以使用記憶體版的 TimingWheelgo-zero 生態元件」。

關於 go-queue 更多的設計和實現文章,可以持續關注我們。歡迎大家去關注和使用。

https://github.com/tal-tech/go-queue

https://github.com/tal-tech/go-zero

歡迎使用 go-zero 並 star 支援我們!

更多原創文章乾貨分享,請關注公眾號
  • 分散式任務 + 訊息佇列框架 go-queue
  • 加微信實戰群請加微信(註明:實戰群):gocnio