一招讓Kafka達到最佳吞吐量

kevwan發表於2021-03-17

通過上一篇文章對 dq 生產者的分析,我們知道 dq 是基於 beanstalk 的封裝。至於 生產者 我們在後續的文章繼續分享,本篇文章先來分析一下 go-queue 中的 kq

kq 基於 kafka 封裝,設計之初是為了使 kafka 的使用更人性化。那就來看看 kq 的使用。

上手使用

func main() {
  // 1. 初始化
    pusher := kq.NewPusher([]string{
        "127.0.0.1:19092",
        "127.0.0.1:19092",
        "127.0.0.1:19092",
    }, "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))
      // 2. 寫入
            if err := pusher.Push(string(body)); err != nil {
                log.Fatal(err)
            }
        }
    }
}

kafka cluster 配置以及 topic 傳入,你就得到一個操作 kafkapush operator

至於寫入訊息,簡單的呼叫 pusher.Push(msg) 就行。是的,就這麼簡單!

當然,目前只支援單個 msg 寫入。可能有人會疑惑,那就繼續往下看,為什麼只能一條一條寫入?

初始化

一起看看 pusher 初始化哪些步驟:

NewPusher(clusterAddrs, topic, opts...)
    |- kafka.NewWriter(kfConfig)                                // 與 kf 之前的連線
    |- executor = executors.NewChunkExecutor()  // 設定內部寫入的executor為位元組數定量寫入
  1. 建立與 kafka cluster 的連線。此處肯定就要傳入 kafka config
  2. 設定內部暫存區的寫入函式以及重新整理規則。

使用 chunkExecutor 作用不言而喻:將隨機寫 -> 批量寫,減少 I/O 消耗;同時保證單次寫入不能超過預設的 1M 或者自己設定的最大寫入位元組數。

其實再往 chunkExecutor 內部看,其實每次觸發插入有兩個指標:

  • maxChunkSize:單次最大寫入位元組數
  • flushInterval:重新整理暫存訊息插入的間隔時間

在觸發寫入,只要滿足任意一個指標都會執行寫入。同時在 executors 都有設定插入間隔時間,以防暫存區寫入阻塞而暫存區內訊息一直不被重新整理清空。

更多關於 executors 可以參看以下:zeromicro.github.io/go-zero/execut...

生產者插入

根據上述初始化對 executors 介紹,插入過程中也少不了它的配合:

func (p *Pusher) Push(v string) error {
  // 1. 將 msg -> kafka 內部的 Message
    msg := kafka.Message{
        Key:   []byte(strconv.FormatInt(time.Now().UnixNano(), 10)),
        Value: []byte(v),
    }

  // 使用 executor.Add() 插入內部的 container
  // 當 executor 初始化失敗或者是內部發生錯誤,也會將 Message 直接插入 kafka
    if p.executor != nil {
        return p.executor.Add(msg, len(v))
    } else {
        return p.produer.WriteMessages(context.Background(), msg)
    }
}

過程其實很簡單。那 executors.Add(msg, len(msg)) 是怎麼把 msg 插入到 kafka 呢?

插入的邏輯其實在初始化中就宣告瞭:

pusher.executor = executors.NewChunkExecutor(func(tasks []interface{}) {
        chunk := make([]kafka.Message, len(tasks))
      // 1
        for i := range tasks {
            chunk[i] = tasks[i].(kafka.Message)
        }
      // 2
        if err := pusher.produer.WriteMessages(context.Background(), chunk...); err != nil {
            logx.Error(err)
        }
    }, newOptions(opts)...)
  1. 觸發插入時,將暫存區中儲存的 []msg 依次拿出,作為最終插入訊息集合;
  2. 將上一步的訊息集合,作為一個批次插入 kafkatopic

這樣 pusher -> chunkExecutor -> kafka 一個鏈路就出現了。下面用一張圖形象表達一下:

image.png

框架地址

github.com/tal-tech/go-queue

同時在 go-queue 也大量使用 go-zero 的 批量處理工具庫 executors

github.com/tal-tech/go-zero

歡迎使用 go-zero & go-queuestar 支援我們!一起構建 go-zero 生態!?

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章