Kafka入門(3):Sarama生產者是如何工作的

紅雞菌發表於2020-08-30

摘要

在這一篇的文章中,我將從Sarama的同步生產者和非同步生產者怎麼建立開始講起,然後我將向你介紹生產者中的各個引數是什麼,怎麼使用。

然後我將從建立生產者的程式碼開始,按照程式碼的呼叫流程慢慢深入,直到傳送訊息並接收到響應。

這個過程跟上面的文章說到的kafka各個層次其實是有對應關係的。

1.如何使用

1.1 介紹

在學習如何使用Sarama生產訊息之前,我先稍微介紹一下。

Sarama有兩種型別的生產者,同步生產者和非同步生產者。

To produce messages, use either the AsyncProducer or the SyncProducer. The AsyncProducer accepts messages on a channel and produces them asynchronously in the background as efficiently as possible; it is preferred in most cases. The SyncProducer provides a method which will block until Kafka acknowledges the message as produced. This can be useful but comes with two caveats: it will generally be less efficient, and the actual durability guarantees depend on the configured value of Producer.RequiredAcks. There are configurations where a message acknowledged by the SyncProducer can still sometimes be lost.

官方文件的大致意思是非同步生產者使用channel接收(生產成功或失敗)的訊息,並且也通過channel來傳送訊息,這樣做通常是效能最高的。而同步生產者需要阻塞,直到收到了acks。但是這也帶來了兩個問題,一是效能變得更差了,而是可靠性是依靠引數acks來保證的。

1.2 非同步傳送

然後我們直接來看看Sarama是怎麼傳送非同步訊息的。

我們先來建立一個最簡陋的非同步生產者,省略所有的不必要的配置。

注意,為了更容易閱讀,我刪去了錯誤處理,並且用省略號替代。

func main() {
	config := sarama.NewConfig()
	client, err := sarama.NewClient([]string{"localhost:9092"}, config)
	...

	producer, err := sarama.NewAsyncProducerFromClient(client)
	...
	defer producer.Close()

	topic := "topic-test"

	for i := 0; i <= 100; i++ {
		text := fmt.Sprintf("message %08d", i)
		producer.Input() <- &sarama.ProducerMessage{
			Topic: topic,
			Key:   nil,
			Value: sarama.StringEncoder(text)}
	}
}

可以看出,Sarama傳送訊息的套路就是先建立一個config,這裡更多的config內容我們會在後文提到。

隨後根據這個config,和broker地址,建立出生產者客戶端。

再然後根據客戶端來建立生產者物件(其實在這裡用物件不夠嚴謹,但是我認為這麼理解是沒有問題的)。

最後就可以使用這個生產者物件來傳送資訊了。

訊息的構造過程中我也省略了其他的引數,只保留了最重要也是最必須的兩個引數:主題和訊息內容。

到了這裡,一個簡單的非同步生產者傳送訊息的過程就結束了。

1.3 同步傳送

在看完了非同步傳送之後,你可能會有很多的諸如“為什麼要這麼做”的疑問。

我們先來看看同步傳送,再來對比一下:

func main() {
	config := sarama.NewConfig()
	config.Producer.Return.Successes = true
	client, err := sarama.NewClient([]string{"localhost:9092"}, config)
	...

	producer, err := sarama.NewSyncProducerFromClient(client)
	...
	defer producer.Close()

	topic := "topic-test"

	for i := 0; i <= 10; i++ {
		text := fmt.Sprintf("message %08d", i)
		partition, offset, err := producer.SendMessage(
			&sarama.ProducerMessage{
				Topic: topic,
				Key:   nil,
				Value: sarama.StringEncoder(text)})
		...
		log.Println("send message success, partition = ", partition, " offset = ", offset)
	}
}

可以看出同步傳送跟非同步傳送的過程是很相似的。

不同的地方在於,同步生產者傳送訊息,使用的不是channel,並且SendMessage方法有三個返回的值,分別為這條訊息的被髮送到了哪個partition,處於哪個offset,是否有error

也就是說,只有在訊息成功的傳送並寫入了broker,才會有返回值。

2. 配置

2.1 預設配置

我們順著原始碼看一下這一行:

config := sarama.NewConfig()

可以看到Sarama已經返回了一個預設的config了:

// NewConfig returns a new configuration instance with sane defaults.
func NewConfig() *Config {
	c := &Config{}

  c.Producer.MaxMessageBytes = 1000000
	c.Producer.RequiredAcks = WaitForLocal
	c.Producer.Timeout = 10 * time.Second
	...
}

2.2 可選配置

我們來看看Config這個結構體,裡面有哪些配置項是允許使用者自定義的。

因為實在是太長了,限於篇幅以及作者的學識,在這篇文章中不能一一講解,所以在這篇文章只會選取部分生產者相關的配置進行講解。

但是無論是Golang客戶端,還是Java客戶端,都不重要,你只需要知道哪些引數對於你的生產者的生產速度、訊息的可靠性等有關係就可以了。

// Config is used to pass multiple configuration options to Sarama's constructors.
type Config struct {
	Admin struct {
		...
	}
	
	Net struct {
		...
	}
	
	Metadata struct {
		...
	}
	
	Producer struct {
		...
	}
	
	Consumer struct {
		...
	}
	
	ClientID string
	
	...
}

我們可以看出,關於Sarama的配置,分成了很多個部分,我們來具體看一看Producer的這部分。

2.3 重要的生產者引數

在這裡我打算介紹一部分我個人認為比較重要的生產者引數。

  • MaxMessageBytes int
    

這個引數影響了一條訊息的最大位元組數,預設是1000000。但是注意,這個引數必須要小於broker中的 message.max.bytes

  • RequiredAcks RequiredAcks
    

這個引數影響了訊息需要被多少broker寫入之後才返回。取值可以是0、1、-1,分別代表了不需要等待broker確認才返回、需要分割槽的leader確認後才返回、以及需要分割槽的所有副本確認後返回。

  • Partitioner PartitionerConstructor
    

這個是分割槽器。Sarama預設提供了幾種分割槽器,如果不指定預設使用Hash分割槽器。

  • Retry
    

這個引數代表了重試的次數,以及重試的時間,主要發生在一些可重試的錯誤中。

  • Flush
    

用於設定將訊息打包傳送,簡單來講就是每次傳送訊息到broker的時候,不是生產一條訊息就傳送一條訊息,而是等訊息累積到一定的程度了,再打包傳送。所以裡面含有兩個引數。一個是多少條訊息觸發打包傳送,一個是累計的訊息大小到了多少,然後傳送。

2.4 冪等生產者

在聊冪等生產者之前,我們先來看看生產者中另外一個很重要的引數:

  • MaxOpenRequests int
    

這個引數代表了允許沒有收到acks而可以同時傳送的最大batch數。

  • Idempotent bool
    

用於冪等生產者,當這一項設定為true的時候,生產者將保證生產的訊息一定是有序且精確一次的。

為什麼會需要這個選項呢?

當MaxOpenRequests這個引數配置大於1的時候,代表了允許有多個請求傳送了還沒有收到回應。假設此時的重試次數也設定為了大於1,當同時傳送了2個請求,如果第一個請求傳送到broker中,broker寫入失敗了,但是第二個請求寫入成功了,那麼客戶端將重新傳送第一個訊息的請求,這個時候會造成亂序。

又比如當第一個請求返回acks的時候,因為網路原因,客戶端沒有收到,所以客戶端進行了重發,這個時候就會造成訊息的重複。

所以,冪等生產者就是為了保證訊息傳送到broker中是有序且不重複的。

訊息的有序可以通過MaxOpenRequests設定為1來保證,這個時候每個訊息必須收到了acks才能傳送下一條,所以一定是有序的,但是不能夠保證不重複。

而且當MaxOpenRequests設定為1的時候,吞吐量不高。

注意,當啟動冪等生產者的時候,Retry次數必須要大於0,ack必須為all。

在Java客戶端中,允許MaxOpenRequests小於等於5。

但是在Sarama中有一個很奇怪的地方我也沒有研究明白,我們直接看一看這部分的程式碼:

if c.Producer.Idempotent {
		if !c.Version.IsAtLeast(V0_11_0_0) {
			return ConfigurationError("Idempotent producer requires Version >= V0_11_0_0")
		}
		if c.Producer.Retry.Max == 0 {
			return ConfigurationError("Idempotent producer requires Producer.Retry.Max >= 1")
		}
		if c.Producer.RequiredAcks != WaitForAll {
			return ConfigurationError("Idempotent producer requires Producer.RequiredAcks to be WaitForAll")
		}
		if c.Net.MaxOpenRequests > 1 {
			return ConfigurationError("Idempotent producer requires Net.MaxOpenRequests to be 1")
		}
	}

這一部分第一項是版本號,沒問題,第二第三項是RetryAcks,也沒有問題。問題在於第四項,這裡的MaxOpenRequests引數,我想應該等同於Java客戶端中的MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,按照Java客戶端中的配置,應該是這個引數小於等於5,即可保證冪等,但是這裡必須得設定為1。

檢查了Sarama的Issue,有開發者提出了這個問題,但是目前作者還沒有打算解決。

3 broker

在這一節的內容中,我將會從程式碼的層面介紹 Sarama 生產者傳送訊息的全過程。但是因為程式碼很多,我將會省略一些內容,包括一些錯誤處理、重試等。

這些都很重要,也不應該被省略。但是因為篇幅有限,我只能介紹最核心的傳送訊息這一部分的內容。

我會在貼程式碼之前,大概的說一下這段程式碼的思路。隨後,我會在程式碼中加入一些註釋,來更詳細的進行解釋。

然後我們開始吧!

producer, err := sarama.NewAsyncProducer([]string{"localhost:9092"}, config)

一切都從這麼一行開始講起。

我們進去看看。

在這裡其實就只有兩個部分,先是通過地址配置,構建一個 client

func NewAsyncProducer(addrs []string, conf *Config) (AsyncProducer, error) {
  
  // 構建client
	client, err := NewClient(addrs, conf)
	if err != nil {
		return nil, err
	}
  
  // 構建AsyncProducer
	return newAsyncProducer(client)
}

3.1 Client的建立

在建立 Client 的過程中,先構建一個 client 結構體。

裡面的引數我們先不管,等用到了再進行解釋。

然後建立完之後,重新整理後設資料,並且啟動一個協程,在後臺進行重新整理。

func NewClient(addrs []string, conf *Config) (Client, error) {
	...
  // 構建一個client
  client := &client{
		conf:                    conf,
		closer:                  make(chan none),
		closed:                  make(chan none),
		brokers:                 make(map[int32]*Broker),
		metadata:                make(map[string]map[int32]*PartitionMetadata),
		metadataTopics:          make(map[string]none),
		cachedPartitionsResults: make(map[string][maxPartitionIndex][]int32),
		coordinators:            make(map[string]int32),
	}
  // 把使用者輸入的broker地址作為“種子broker”增加到seedBrokers中
  // 隨後客戶端會根據已有的broker地址,自動重新整理後設資料,以獲取更多的broker地址
  // 所以稱之為種子
  random := rand.New(rand.NewSource(time.Now().UnixNano()))
	for _, index := range random.Perm(len(addrs)) {
		client.seedBrokers = append(client.seedBrokers, NewBroker(addrs[index]))
	}
	...
  // 啟動協程在後臺重新整理後設資料
  go withRecover(client.backgroundMetadataUpdater)
  return client, nil
}
  

3.2 後設資料的更新

後臺更新後設資料的設計其實很簡單,利用一個 ticker ,按時對後設資料進行更新,直到 client 關閉。

這裡先提一下我們說的後設資料,有哪些內容。

你可以簡單的理解為包含了所有 broker 的地址(因為 broker 可能新增,也可能減少),以及包含了哪些 topic ,這些 topic 有哪些 partition 等。

func (client *client) backgroundMetadataUpdater() {
  
  // 按照配置的時間更新後設資料
  ticker := time.NewTicker(client.conf.Metadata.RefreshFrequency)
	defer ticker.Stop()
  
  // 迴圈獲取channel,判斷是執行更新操作還是終止
  for {
		select {
		case <-ticker.C:
			if err := client.refreshMetadata(); err != nil {
				Logger.Println("Client background metadata update:", err)
			}
		case <-client.closer:
			return
		}
	}
}

然後我們繼續來看看 client.refreshMetadata() 這個方法,這個方法是判斷了一下需要重新整理哪些主題的後設資料,還是說全部主題的後設資料。

然後我們繼續。

在這裡也還沒有涉及到具體的更新操作。我們看 tryRefreshMetadata 這個方法的引數可以得知,在這裡我們設定了需要重新整理後設資料的主題,重試的次數,超時的時間。

func (client *client) RefreshMetadata(topics ...string) error {
  deadline := time.Time{}
	if client.conf.Metadata.Timeout > 0 {
		deadline = time.Now().Add(client.conf.Metadata.Timeout)
	}
  // 設定引數
	return client.tryRefreshMetadata(topics, client.conf.Metadata.Retry.Max, deadline)
}

然後終於來到了tryRefreshMetadata這個方法。

在這個方法中,會選取已經存在的broker,構造獲取後設資料的請求。

在收到回應後,如果不存在任何的錯誤,就將這些後設資料用於更新客戶端。

func (client *client) tryRefreshMetadata(topics []string, attemptsRemaining int, deadline time.Time) error {
  ...
  
  broker := client.any()
	for ; broker != nil && !pastDeadline(0); broker = client.any() {
    ...
    		req := &MetadataRequest{
          Topics: topics, 
          // 是否允許建立不存在的主題
          AllowAutoTopicCreation: allowAutoTopicCreation
        }
    response, err := broker.GetMetadata(req)
    switch err.(type) {
		case nil:
			allKnownMetaData := len(topics) == 0
      // 對後設資料進行更新
			shouldRetry, err := client.updateMetadata(response, allKnownMetaData)
			if shouldRetry {
				Logger.Println("client/metadata found some partitions to be leaderless")
				return retry(err)
			}
			return err
		case ...
      ...
    }
  }

然後我們繼續往下看看當客戶端拿到了 response 之後,是如何更新的。

首先,先對本地儲存 broker 進行更新。

然後,對 topic 進行更新,以及這個 topic 下面的那些 partition

func (client *client) updateMetadata(data *MetadataResponse, allKnownMetaData bool) (retry bool, err error) {
  ...
  // 假設返回了新的broker id,那麼儲存這些新的broker,這意味著增加了broker、或者下線的broker重新上線了
  // 如果返回的id我們已經儲存了,但是地址變化了,那麼更新地址
  // 如果本地儲存的一些id沒有返回,說明這些broker下線了,那麼刪除他們
  client.updateBroker(data.Brokers)
  
  // 然後對topic也進行後設資料的更新
  // 主要是更新topic以及topic對應的partition
  for _, topic := range data.Topics {
    ...
    // 更新每個topic以及對應的partition
    client.metadata[topic.Name] = make(map[int32]*PartitionMetadata, len(topic.Partitions))
		for _, partition := range topic.Partitions {
			client.metadata[topic.Name][partition.ID] = partition
			...
		}
  }

至此,我們後設資料的更新就說完了。

下面我們來說一說在更新後設資料之前,broker是如何建立連線的,以及請求是如何傳送出去,又是如何被broker接收的。

3.3 與Broker建立連線

讓我們回到 tryRefreshMetadata 這個方法中。

這個方法裡面有這麼一行程式碼:

broker := client.any()

我們進去看看。

在這個方法裡, 如果 seedBrokers 存在,那麼就開啟它,否則的話開啟其他的broker。

注意,這裡提到的其他的broker,可能是在重新整理後設資料的時候,獲取到的。這就跟上面的內容聯絡在一起了。

func (client *client) any() *Broker {
	...
	if len(client.seedBrokers) > 0 {
		_ = client.seedBrokers[0].Open(client.conf)
		return client.seedBrokers[0]
	}

	// 不保證一定是按順序的
	for _, broker := range client.brokers {
		_ = broker.Open(client.conf)
		return broker
	}

	return nil
}

然後再讓我們看看 Open方法做了什麼。

Open方法非同步的建立了一個tcp連線,然後建立了一個緩衝大小為MaxOpenRequestschannel

這個名為 responseschannel ,用於接收從 broker傳送回來的訊息。

其實在 broker 中,用於傳送訊息跟接收訊息的 channel 都設定成了這個大小。

MaxOpenRequests 這個引數你可以理解為是Java客戶端中的max.in.flight.requests.per.connection

然後,又啟動了一個協程,用於接收訊息。

func (b *Broker) Open(conf *Config) error {
  if conf == nil {
		conf = NewConfig()
	}
  ...
  go withRecover(func() {
    ...
    dialer := conf.getDialer()
		b.conn, b.connErr = dialer.Dial("tcp", b.addr)
    
    ...
    b.responses = make(chan responsePromise, b.conf.Net.MaxOpenRequests-1)
    ...
    go withRecover(b.responseReceiver)
  })

3.4 從Broker接收響應

我們來看看 responseReceiver 是怎麼工作的。

其實很容易理解,當 broker 收到一個 response 的時候,先解析訊息的頭部,然後再解析訊息的內容。並把這些內容寫進 responsepackets 中。

func (b *Broker) responseReceiver() {
  for response := range b.responses {
    
    ...
    // 先根據Header的版本讀取對應長度的Header
    var headerLength = getHeaderLength(response.headerVersion)
		header := make([]byte, headerLength)
		bytesReadHeader, err := b.readFull(header)
    decodedHeader := responseHeader{}
		err = versionedDecode(header, &decodedHeader, response.headerVersion)
    
    ...
    // 解析具體的內容
    buf := make([]byte, decodedHeader.length-int32(headerLength)+4)
		bytesReadBody, err := b.readFull(buf)
    
    // 省略了一些錯誤處理,總之,如果發生了錯誤,就把錯誤資訊寫進 response.errors 中
    response.packets <- buf
  }
}

其實接收響應這部分的程式碼邏輯很容易理解,就是當 response 這個 channel 有了訊息,就讀取,然後將讀取到的內容寫進 response 中。

那麼你可能會有一個問題,什麼時候才會往response 這個 channel 傳送訊息呢?

很容易可以猜到,當我們傳送了訊息給 broker ,就應該要通知 receiver ,準備接受訊息了。

既然如此,我們繼續剛剛重新整理後設資料的部分,看看 sarama 是如何把訊息傳送出去的。

3.5 傳送與接受訊息

我們回到這一行程式碼:

response, err := broker.GetMetadata(req)

我們直接進去,發現在這裡構造了一個接受返回資訊的結構體,然後呼叫了sendAndReceive方法。

func (b *Broker) GetMetadata(request *MetadataRequest) (*MetadataResponse, error) {
	response := new(MetadataResponse)

	err := b.sendAndReceive(request, response)

	if err != nil {
		return nil, err
	}

	return response, nil
}

我們繼續往下。

在這裡我們可以看到,先是呼叫了send方法,然後返回了一個promise。並且當有訊息寫入這個promise的時候,就得到了結果。

而且回想一下我們在receiver中,是不是把獲取到的 response 寫進了 packets ,把錯誤結果寫進了 errors 呢,跟這裡是一致的對吧?

func (b *Broker) sendAndReceive(req protocolBody, res protocolBody) error {
	responseHeaderVersion := int16(-1)
	if res != nil {
		responseHeaderVersion = res.headerVersion()
	}

	promise, err := b.send(req, res != nil, responseHeaderVersion)
	if err != nil {
		return err
	}

	if promise == nil {
		return nil
	}

  // 這裡的promise,是上面send方法返回的
	select {
	case buf := <-promise.packets:
		return versionedDecode(buf, res, req.version())
	case err = <-promise.errors:
		return err
	}
}

帶著這個想法,我們看看 send 方法做了什麼事。

這個地方很重要,也是我認為 Sarama 設計的特別巧妙的一個地方。

在send方法中,把需要傳送的訊息通過與broker的tcp連線,同步傳送到broker中。

然後構建了一個responsePromise型別的channel,然後直接將這個結構體丟進這個channel中。然後回想一下,我們在responseReceiver這個方法中,不斷消費接收到的response。

此時在responseReceiver中,收到了send方法傳遞的responsePromise,他就會通過conn來讀取資料,然後將資料寫入這個responsePromise的packets中,或者將錯誤資訊寫入errors中。

而此時,再看看send方法,他返回了這個responsePromise的指標。所以,sendAndReceive方法就在等待這個responsePromise內的packets或者errors的channel被寫入資料。當responseReceiver接收到了響應並且寫入資料的時候,packets或者errors就會被寫入訊息。

func (b *Broker) send(rb protocolBody, promiseResponse bool, responseHeaderVersion int16) (*responsePromise, error) {
  
  ...
  // 將請求的內容封裝進 request ,然後傳送到Broker中
  // 注意一下這裡的 b.write(buf) 
  // 裡面做了 b.conn.Write(buf) 這件事情
  req := &request{correlationID: b.correlationID, clientID: b.conf.ClientID, body: rb}
	buf, err := encode(req, b.conf.MetricRegistry)
  bytes, err := b.write(buf)
  
  ...
  // 如果我們的response為nil,也就是說當不需要response的時候,是不會放進inflight傳送佇列的
  if !promiseResponse {
		// Record request latency without the response
		b.updateRequestLatencyAndInFlightMetrics(time.Since(requestTime))
		return nil, nil
	}
  
  // 構建一個接收響應的 channel ,返回這個channel的指標
  // 這個 channel 內部包含了兩個 channel,一個用來接收響應,一個用來接收錯誤 
  promise := responsePromise{requestTime, req.correlationID, responseHeaderVersion, make(chan []byte), make(chan error)}
	b.responses <- promise

  // 這裡返回指標特別的關鍵,是把訊息的傳送跟訊息的接收聯絡在一起了
	return &promise, nil
}

讓我們來用一張圖說明一下上面這個傳送跟接收的過程:

這一段比較繞,但這也是Sarama傳送與接受訊息的核心內容,希望我的解釋能夠讓你理解:)

4 AsyncProcuder

在上一節中,我們已經分析了client的構造全過程,並且在構造client重新整理後設資料的時候,也解釋了sarama是如何傳送訊息以及接受訊息的。

在這一節中,我打算解釋一下AsyncProcuder是如何傳送訊息的。

因為有了上一節的鋪墊,這一節的內容應該會比較容易理解。

我們從newAsyncProducer(client)這一行開始講起。

我們先說說input:make(chan *ProducerMessage),這個事關我們的訊息傳送。注意到這個channel是沒有緩衝的。

也就是說當我們傳送一條訊息到input中的時候,此時傳送方會阻塞,這說明了之後的操作必須不能夠被阻塞,否則會影響訊息的傳送效率。

然後其他欄位我們先不管,後面用到了我們再提。

func newAsyncProducer(client Client) (AsyncProducer, error) {
  ...
  p := &asyncProducer{
		client:     client,
		conf:       client.Config(),
		errors:     make(chan *ProducerError),
		input:      make(chan *ProducerMessage),
		successes:  make(chan *ProducerMessage),
		retries:    make(chan *ProducerMessage),
		brokers:    make(map[*Broker]*brokerProducer),
		brokerRefs: make(map[*brokerProducer]int),
		txnmgr:     txnmgr,
	}
  
  go withRecover(p.dispatcher)
	go withRecover(p.retryHandler)
}

4.1 dispatcher

我們往下看看下面協程啟動的go withRecover(p.dispatcher)

在這個方法中,首先建立了一個以Topic為key的map,這個map的value是無緩衝的channel。

到這裡我們很容易可以推測得出,當通過input傳送一條訊息的時候,訊息會到dispatcher這裡,被分配到各個Topic中。

注意,在這個時候,channel還是無緩衝的,所以我們可以推測下一步的操作,依舊是無阻塞的。

func (p *asyncProducer) dispatcher() {
  handlers := make(map[string]chan<- *ProducerMessage)
  ...
  for msg := range p.input {
    ...
    // 攔截器
    for _, interceptor := range p.conf.Producer.Interceptors {
			msg.safelyApplyInterceptor(interceptor)
		}
    
    ...
    // 找到這個Topic對應的Handler
    handler := handlers[msg.Topic]
		if handler == nil {
      // 如果此時還不存在這個Topic對應的Handler,那麼建立一個
      // 雖然說他叫Handler,但他其實是一個無緩衝的
			handler = p.newTopicProducer(msg.Topic)
			handlers[msg.Topic] = handler
		}
		// 然後把這條訊息寫進這個Handler中
		handler <- msg
  }
}

然後讓我們來handler = p.newTopicProducer(msg.Topic)這一行的程式碼。

在這裡建立了一個緩衝大小為ChannelBufferSize的channel,用於存放傳送到這個主題的訊息。

然後建立了一個topicProducer,在這個時候你可以認為訊息已經交付給各個topic的topicProducer了。

func (p *asyncProducer) newTopicProducer(topic string) chan<- *ProducerMessage {
	input := make(chan *ProducerMessage, p.conf.ChannelBufferSize)
	tp := &topicProducer{
		parent:      p,
		topic:       topic,
		input:       input,
		breaker:     breaker.New(3, 1, 10*time.Second),
		handlers:    make(map[int32]chan<- *ProducerMessage),
		partitioner: p.conf.Producer.Partitioner(topic),
	}
	go withRecover(tp.dispatch)
	return input
}

4.2 topicDispatch

然後我們來看看go withRecover(tp.dispatch)這一行程式碼。

同樣是啟動了一個協程,來處理訊息。

也就是說,到了這一步,對於每一個Topic,都有一個協程來處理訊息。

在這個dispatch()方法中,也同樣的接收到一條訊息,就會去找這條訊息所在的分割槽的channel,然後把訊息寫進去。

func (tp *topicProducer) dispatch() {
  for msg := range tp.input {
    ...
    
    // 同樣是找到這條訊息所在的分割槽對應的channel,然後把訊息丟進去
    handler := tp.handlers[msg.Partition]
		if handler == nil {
			handler = tp.parent.newPartitionProducer(msg.Topic, msg.Partition)
			tp.handlers[msg.Partition] = handler
		}

		handler <- msg
  }
}

4.3 PartitionDispatch

我們進tp.parent.newPartitionProducer(msg.Topic, msg.Partition)這裡看看。

你可以發現partitionProducer跟topicProducer是很像的。

其實他們就是代表了一條訊息的分發,從producer到topic到partition。

注意,這裡面的channel緩衝大小,也是ChannelBufferSize。

func (p *asyncProducer) newPartitionProducer(topic string, partition int32) chan<- *ProducerMessage {
	input := make(chan *ProducerMessage, p.conf.ChannelBufferSize)
	pp := &partitionProducer{
		parent:    p,
		topic:     topic,
		partition: partition,
		input:     input,

		breaker:    breaker.New(3, 1, 10*time.Second),
		retryState: make([]partitionRetryState, p.conf.Producer.Retry.Max+1),
	}
	go withRecover(pp.dispatch)
	return input
}

4.4 partitionProducer

到了這一步,我們再來看看訊息到了每個partition所在的channel,是如何處理的。

其實在這一步中,主要是做一些錯誤處理之類的,然後把訊息丟進brokerProducer。

可以理解為這一步是業務邏輯層到網路IO層的轉變,在這之前我們只關心訊息去到了哪個分割槽,而在這之後,我們需要找到這個分割槽所在的broker的地址,並使用之前已經建立好的TCP連線,傳送這條訊息。

func (pp *partitionProducer) dispatch() {
  
  // 找到這個主題和分割槽的leader所在的broker
  pp.leader, _ = pp.parent.client.Leader(pp.topic, pp.partition)
  // 如果此時找到了這個leader
  if pp.leader != nil {
		pp.brokerProducer = pp.parent.getBrokerProducer(pp.leader)
		pp.parent.inFlight.Add(1) 
    // 傳送一條訊息來表示同步
		pp.brokerProducer.input <- &ProducerMessage{Topic: pp.topic, Partition: pp.partition, flags: syn}
	}
  ...// 各種異常情況
  
  // 然後把訊息丟進brokerProducer中
  pp.brokerProducer.input <- msg
}

4.5 brokerProducer

到了這裡,大概算是整個傳送流程最後的一個步驟了。

我們來看看pp.parent.getBrokerProducer(pp.leader)這行程式碼裡面的內容。

其實就是找到asyncProducer中的brokerProducer,如果不存在,則建立一個。

func (p *asyncProducer) getBrokerProducer(broker *Broker) *brokerProducer {
	p.brokerLock.Lock()
	defer p.brokerLock.Unlock()

	bp := p.brokers[broker]

	if bp == nil {
		bp = p.newBrokerProducer(broker)
		p.brokers[broker] = bp
		p.brokerRefs[bp] = 0
	}

	p.brokerRefs[bp]++

	return bp
}

那我們就來看看brokerProducer是怎麼建立出來的。

看這個方法中啟動的第二個協程,我們可以推測bridge這個channel收到訊息後,會把收到的訊息打包成一個request,然後呼叫Produce方法。

並且,將返回的結果的指標地址,寫進response中。

然後構造好brokerProducerResponse,並且寫入responses中。

func (p *asyncProducer) newBrokerProducer(broker *Broker) *brokerProducer {
	var (
		input     = make(chan *ProducerMessage)
		bridge    = make(chan *produceSet)
		responses = make(chan *brokerProducerResponse)
	)

	bp := &brokerProducer{
		parent:         p,
		broker:         broker,
		input:          input,
		output:         bridge,
		responses:      responses,
		stopchan:       make(chan struct{}),
		buffer:         newProduceSet(p),
		currentRetries: make(map[string]map[int32]error),
	}
	go withRecover(bp.run)

	// minimal bridge to make the network response `select`able
	go withRecover(func() {
		for set := range bridge {
			request := set.buildRequest()

			response, err := broker.Produce(request)

			responses <- &brokerProducerResponse{
				set: set,
				err: err,
				res: response,
			}
		}
		close(responses)
	})

	if p.conf.Producer.Retry.Max <= 0 {
		bp.abandoned = make(chan struct{})
	}

	return bp
}

讓我們再來看看broker.Produce(request)這一行程式碼。

是不是很熟悉呢,我們在client部分講到的sendAndReceive方法。

而且我們可以發現,如果我們設定了需要Acks,就會返回一個response;如果沒設定,那麼訊息發出去之後,就不管了。

此時在獲取了response,並且填入了response的內容後,返回這個response的內容。

func (b *Broker) Produce(request *ProduceRequest) (*ProduceResponse, error) {
	var (
		response *ProduceResponse
		err      error
	)

	if request.RequiredAcks == NoResponse {
		err = b.sendAndReceive(request, nil)
	} else {
		response = new(ProduceResponse)
		err = b.sendAndReceive(request, response)
	}

	if err != nil {
		return nil, err
	}

	return response, nil
}

至此,Sarama生產者相關的內容就介紹完畢了。

寫在最後

這一篇寫的實在是有些久了。

主要是作者這段時間實在是太忙了,還沒有完全平衡好目前的學習工作和生活,導致每天花在學習上的時間不多,效率也不高。

另外就是網上我也沒有查到有Sarama相關的解析,都是一些API的呼叫。因為作者恰好開始學習Kafka,為了更好地瞭解生產者的每一個引數,我選擇去研究生產者客戶端。

但是,因為作者原始碼閱讀能力實在是有限,在這個過程中很有可能會有一些錯誤的理解。所以當你發現了一些違和的地方,也請不吝指教,謝謝你!

再次感謝你能看到這裡!

PS:如果有其他的問題,也可以在公眾號找到我,歡迎來找我玩~

相關文章