Strimzi Kafka Bridge(橋接)實戰之三:自制sdk(golang版本)

程式設計師欣宸發表於2023-10-07

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本文是《Strimzi Kafka Bridge(橋接)實戰》的第三篇,前文我們們掌握了Strimzi Kafka Bridge的基本功能:基於http提供各種kafka訊息的服務
  • 此刻,如果想透過http介面呼叫bridge的服務,勢必要寫不少程式碼(請求資料的生成、響應資料的解析),好在Strimzi已經提供了標準OpenApi的配置檔案,我們們可以根據這個配置檔案生成與http介面相關的程式碼,省去不少工作

為什麼是golang版本

  • 熟悉欣宸的讀者都知道欣宸是個正宗的java程式設計師,那麼,本篇應該實戰java版本的SDK吧,怎麼就研究起了golang版本呢?
  • 因為Strimzi Kafka Bridge提供的OpenApi配置,用來生成客戶端sdk之後,是無法正常使用的!!!,沒錯,您沒看錯,用工具生成的sdk,不論是golang版還是java版,都用不了!
  • 相比之下,golang版的sdk,雖然不能用,但是經過搶救還是可以正常工作的,這也是本篇的主要內容
  • 而java版的就沒那麼幸運了,涉及到jar庫的依賴,就算是改程式碼也救不活,於是只能放棄,具體的原因本文末尾會給出,當然了,也許是欣宸水平太差,換成其他高手說不定就給救活了
  • 閒話少說,接下來的內容由以下這幾個步驟組成
  1. 介紹一下我這邊的環境資訊
  2. 下載OpenApi的配置檔案
  3. 下載swagger工具
  4. 用swagger工具生成客戶端sdk程式碼
  5. 建立一個golang的demo程式,使用剛剛生成的客戶端sdk程式碼
  6. 客戶端sdk程式碼存在諸多問題,但是可以逐個修復,這裡我們們就來修復它們
  7. 執行一個demo程式,呼叫sdk程式碼中的API,驗證基本功能

環境資訊

  • 以下是我這邊的環境資訊,您可以作為參考
  1. JDK:11.0.14.1
  2. Maven:3.8.5
  3. strimzi-kafka-bridge:0.22.3
  4. swagger-codegen-cli:2.4.9
  • 需要注意的是,swagger工具是jar格式的,因此需要當前環境準備好JDK

下載OpenApi的配置檔案

  • Strimzi Kafka Bridge的master分支處於活躍狀態,因此不適合拿來實戰,我們們選擇一個釋出版本吧
  • 下載strimzi-kafka-bridge原始碼,地址是:https://codeload.github.com/strimzi/strimzi-kafka-bridge/zip/refs/tags/0.22.3 ,下載後解壓得到名為strimzi-kafka-bridge-0.22.3的資料夾
  • 這個檔案就是OpenApi的配置檔案,可以用來生成客戶端sdk原始碼:strimzi-kafka-bridge-0.22.3/src/main/resources/openapiv2.json ,稍後會用到

下載swagger工具

用swagger工具生成客戶端sdk程式碼

  • 使用預設引數來生成客戶端sdk程式碼的操作十分簡單
java -jar swagger-codegen-cli-2.4.9.jar generate \
-i ./openapiv2.json \
-l go \
-o swagger
  • 執行完命令後,控制檯輸出如下
    在這裡插入圖片描述

  • 檢視swagger目錄,發現已經生成了大量檔案

➜  001 tree swagger
swagger
├── README.md
├── api
│   └── swagger.yaml
├── api_consumers.go
├── api_default.go
├── api_producer.go
├── api_seek.go
├── api_topics.go
├── client.go
├── configuration.go
├── docs
│   ├── AssignedTopicPartitions.md
│   ├── BridgeInfo.md
│   ├── Consumer.md
│   ├── ConsumerRecord.md
│   ├── ConsumerRecordList.md
│   ├── ConsumersApi.md
│   ├── CreatedConsumer.md
│   ├── DefaultApi.md
│   ├── KafkaHeader.md
│   ├── KafkaHeaderList.md
│   ├── ModelError.md
│   ├── OffsetCommitSeek.md
│   ├── OffsetCommitSeekList.md
│   ├── OffsetRecordSent.md
│   ├── OffsetRecordSentList.md
│   ├── OffsetsSummary.md
│   ├── Partition.md
│   ├── PartitionMetadata.md
│   ├── Partitions.md
│   ├── ProducerApi.md
│   ├── ProducerRecord.md
│   ├── ProducerRecordList.md
│   ├── ProducerRecordToPartition.md
│   ├── ProducerRecordToPartitionList.md
│   ├── Replica.md
│   ├── SeekApi.md
│   ├── SubscribedTopicList.md
│   ├── TopicMetadata.md
│   ├── Topics.md
│   └── TopicsApi.md
├── git_push.sh
├── model_assigned_topic_partitions.go
├── model_bridge_info.go
├── model_consumer.go
├── model_consumer_record.go
├── model_consumer_record_list.go
├── model_created_consumer.go
├── model_error.go
├── model_kafka_header.go
├── model_kafka_header_list.go
├── model_offset_commit_seek.go
├── model_offset_commit_seek_list.go
├── model_offset_record_sent.go
├── model_offset_record_sent_list.go
├── model_offsets_summary.go
├── model_partition.go
├── model_partition_metadata.go
├── model_partitions.go
├── model_producer_record.go
├── model_producer_record_list.go
├── model_producer_record_to_partition.go
├── model_producer_record_to_partition_list.go
├── model_replica.go
├── model_subscribed_topic_list.go
├── model_topic_metadata.go
├── model_topics.go
└── response.go

2 directories, 66 files

建立一個golang的demo程式,使用剛剛生成的客戶端sdk程式碼

  • 新建名為sdkdemo的資料夾
  • sdkdemo的資料夾下面執行以下命令,新建一個go工程
go mod init sdkdemo
  • 需要引入兩個包,執行以下命令
go get golang.org/x/oauth2
go get github.com/antihax/optional
  • 將前面生成程式碼的swagger資料夾複製到sdkdemo的資料夾下面

  • 現在sdkdemo的資料夾下面有這些東西
    在這裡插入圖片描述

  • 為了方便開發,接下來用IDE工具進行開發,我這裡用的是goland,開啟專案後新增名為main.go的檔案
    在這裡插入圖片描述

  • 接下來我們們要面對的是一堆破綻百出的sdk程式碼,不過還好,可以拯救,我們們一起啦拯救吧

修復有問題的sdk原始碼,第一個問題

  • 一共有6個問題,我們們逐一修復
  • 第一個問題如下圖,SeekToEndOpts這個資料結構在api_seek.goapi_consumer.go中都有,顯然是重複定義了,將左側api_seek.go中的SeekToEndOpts定義刪除掉
    在這裡插入圖片描述

第二個問題

  • 第二個問題如下圖,SendOpts這個資料結構在api_topics.goapi_producer.go中都有,顯然是重複定義了,將左側api_topics.go中的SeekToEndOpts定義刪除掉
    在這裡插入圖片描述

第三個問題

  • 第三個問題最讓人痛苦(因為java版也被此問題折磨,且不好處理),bridge的請求和響應的contentType,與我們們平時常用的application/json不同,在bridge這裡用的是這兩種:application/vnd.kafka.v2+jsonapplication/vnd.kafka.json.v2+json,其實這個也好理解:生產和傳送的訊息內容不一定只有json格式,可能還會嵌入其他格式的訊息,這就要有kafka自己的協議來支援了,於是contentType就變得比較特殊
  • 話雖這麼說,但是swagger不認識application/vnd.kafka.v2+jsonapplication/vnd.kafka.json.v2+json這兩種格式,於是生成的程式碼自然也就不支援了
  • 來看看具體問題吧,開啟檔案client.go,當前decode方法原始碼如下,可見是不會處理application/vnd.kafka.v2+jsonapplication/vnd.kafka.json.v2+json這兩種的
func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
	if strings.Contains(contentType, "application/xml") {
		if err = xml.Unmarshal(b, v); err != nil {
			return err
		}
		return nil
	} else if strings.Contains(contentType, "application/json") {
		if err = json.Unmarshal(b, v); err != nil {
			return err
		}
		return nil
	}
	return errors.New("undefined response type")
}
  • 把程式碼改成下面這樣,對application/vnd.kafka.v2+jsonapplication/vnd.kafka.json.v2+json這兩種型別的資料,處理方法都等同於json
func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
	if strings.Contains(contentType, "application/xml") {
		if err = xml.Unmarshal(b, v); err != nil {
			return err
		}
		return nil
	} else if strings.Contains(contentType, "application/json") ||
		strings.Contains(contentType, "application/vnd.kafka.v2+json") ||
		strings.Contains(contentType, "application/vnd.kafka.json.v2+json") {
		if err = json.Unmarshal(b, v); err != nil {
			return err
		}
		return nil
	}
	return errors.New("undefined response type")
}
  • 當然了這樣做的弊端也很明顯:只支援json格式的內容,kakfa原本支援的多種格式都不能處理了

第四個問題

  • 第四個問題也和contentType有關,前面第三個問題發生在請求階段,而第四個問題發生在處理響應資料的階段
  • 還是client.go檔案,這次是setBody方法,先看看原始內容
// Set request body from an interface{}
func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) {
	if bodyBuf == nil {
		bodyBuf = &bytes.Buffer{}
	}

	if reader, ok := body.(io.Reader); ok {
		_, err = bodyBuf.ReadFrom(reader)
	} else if b, ok := body.([]byte); ok {
		_, err = bodyBuf.Write(b)
	} else if s, ok := body.(string); ok {
		_, err = bodyBuf.WriteString(s)
	} else if s, ok := body.(*string); ok {
		_, err = bodyBuf.WriteString(*s)
	} else if jsonCheck.MatchString(contentType) {
		err = json.NewEncoder(bodyBuf).Encode(body)
	} else if xmlCheck.MatchString(contentType) {
		xml.NewEncoder(bodyBuf).Encode(body)
	}

	if err != nil {
		return nil, err
	}

	if bodyBuf.Len() == 0 {
		err = fmt.Errorf("Invalid body type %s\n", contentType)
		return nil, err
	}
	return bodyBuf, nil
}
  • 修改後的內容如下圖,紅色箭頭所指為新增內容
    在這裡插入圖片描述

第五個問題

  • 第五個問題,簡直是strimzi拿來噁心開發者的,在拉取訊息的時候,bridge的server端只支援application/vnd.kafka.json.v2+json,結果在OpenApi中卻定義了多種型別,結果拉去訊息的時候,bridge會提示多出的型別不支援
  • 這個問題可以用postman等工具復現,如下圖
    在這裡插入圖片描述
  • 程式碼的改動如下圖,修改api_consumers.go
    在這裡插入圖片描述

第六個問題

  • 最後一個問題是資料結構定義問題,開啟model_consumer_record_list.go,看到內容如下,真夠壞的,挖這麼大的坑...
package swagger

type ConsumerRecordList struct {
}
  • 改成這樣就好了
package swagger

type ConsumerRecordList []ConsumerRecord

第七個問題

  • 第七個問題,也是挖了個坑讓我跳,開啟檔案model_producer_record.go,內容如下,根據前一篇的請求內容,可知這裡缺少兩個欄位:KeyValue
package swagger

type ProducerRecord struct {
	Partition int32 `json:"partition,omitempty"`
	Headers *KafkaHeaderList `json:"headers,omitempty"`
}
  • 修改後如下
package swagger

type ProducerRecord struct {
	Partition int32 `json:"partition,omitempty"`
	Value string `json:"value"`
	Key string `json:"key,omitempty"`
	Headers *KafkaHeaderList `json:"headers,omitempty"`
}

第八個問題

  • 最後一個問題,是在提交offset的時候,bridge後臺不接受contentType,所以請開啟檔案api_consumers.go,修改如下,註釋掉一行程式碼
    在這裡插入圖片描述

  • 坑已經填完了,開始驗證SDK能不能用吧

編寫程式碼驗證功能:檢視topic列表

  • 開啟main.go檔案,增加以下內容,都是要用到的常量,以及sdk配置的初始化
// 測試用的topic
const TEST_TOPIC = "bridge-quickstart-topic"

const TEST_GROUP = "client-sdk-group"

const CONSUMER_NAME = "client-sdk-consumer-002"

// strimzi bridge地址
const BASE_PATH = "http://127.0.0.1:31331"

var client *swagger.APIClient

func init() {
	configuration := swagger.NewConfiguration()
	configuration.BasePath = BASE_PATH
	client = swagger.NewAPIClient(configuration)
}
  • 呼叫SDK來檢視kafka的topic列表的程式碼如下
func getAllTopics() ([]string, error) {
	array, response, err := client.TopicsApi.ListTopics(context.Background())

	if err != nil {
		log.Printf("getAllTopics err: %v\n", err)
		return nil, err
	}

	log.Printf("response: %v", response)

	return array, nil
}
  • 在main方法中呼叫getAllTopics
func main() {
	topics, err := getAllTopics()
	if err != nil {
		return
	}

	fmt.Printf("topics: %v\n", topics)
}
  • 執行main方法,結果如下,可見成功獲取到topic列表,sdk能用
2022/12/18 21:26:33 response: &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[109] Content-Type:[application/vnd.kafka.v2+json]] 0x140000e0300 109 [] false false map[] 0x14000118100 <nil>}
topics: [__strimzi_store_topic bridge-quickstart-topic __strimzi-topic-operator-kstreams-topic-store-changelog]

Process finished with the exit code 0

編寫程式碼驗證功能:傳送訊息

  • 傳送訊息的程式碼如下
// 傳送訊息(非同步模式,不會收到offset返回)
func sendAsync(info string) error {
	log.Print("send [" + info + "]")
	_, response, err := client.ProducerApi.Send(context.Background(),
		TEST_TOPIC,
		swagger.ProducerRecordList{
			Records: []swagger.ProducerRecord{
				{Value: "message from go swagger SDK"},
			},
		},
		&swagger.SendOpts{Async: optional.NewBool(true)},
	)

	if err != nil {
		log.Printf("send err: %v\n", err)
		return err
	}

	log.Printf("response: %v", response.StatusCode)

	return nil
}
  • 把main方法改成下面這樣,連續呼叫傳送訊息的請求
func main() {
	for i := 0; i < 10; i++ {
		sendAsync("message from go client " + strconv.Itoa(i))
	}
}
  • 控制檯輸出如下,可見傳送訊息成功,稍後我們們還會寫消費的程式碼來消費這些訊息
/private/var/folders/5v/p3bj9bzx2nd99y5l21nb1c080000gn/T/GoLand/___go_build_sdkdemo
2022/12/18 21:35:47 send [message from go client 0]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 1]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 2]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 3]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 4]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 5]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 6]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 7]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 8]
2022/12/18 21:35:47 response: 204
2022/12/18 21:35:47 send [message from go client 9]
2022/12/18 21:35:47 response: 204

Process finished with the exit code 0

編寫程式碼驗證功能:建立consumer

  • 先增加兩個輔助方法,用來處理特別的包體和錯誤資訊
// 取出swagger特有的error型別,從中提取中有效的錯誤資訊
func getErrorMessage(err error) string {
	e := err.(swagger.GenericSwaggerError)
	return string(e.Body())
}

func getBodyStr(body io.ReadCloser) string {
	buf := new(bytes.Buffer)
	buf.ReadFrom(body)
	return buf.String()
}

  • 建立consumer的程式碼如下
// 建立consumer
func CreateConsumer(group string, consumerName string) (*swagger.CreatedConsumer, error) {

	consumer, response, err := client.ConsumersApi.CreateConsumer(context.Background(),
		group,
		swagger.Consumer{
			Name:                     consumerName,
			AutoOffsetReset:          "latest",
			FetchMinBytes:            16,
			ConsumerRequestTimeoutMs: 300 * 1000,
			EnableAutoCommit:         false,
			Format:                   "json",
		})

	if err != nil {
		log.Printf("CreateConsumer error : %v", getErrorMessage(err))
		return nil, err
	}

	log.Printf("CreateConsumer response : %v, body [%v]", response, getBodyStr(response.Body))
	log.Printf("consumer : %v", consumer)
	return &consumer, nil
}
  • 在main方法中呼叫,即可建立consumer
func main() {
	// 建立consumer
	CreateConsumer(TEST_GROUP, CONSUMER_NAME)
}

編寫程式碼驗證功能:訂閱

  • 訂閱程式碼如下
// 訂閱
func Subsciribe(topic string, consumerGroup string, consumerName string) error {

	response, err := client.ConsumersApi.Subscribe(context.Background(),
		swagger.Topics{Topics: []string{topic}},
		consumerGroup,
		consumerName,
	)

	if err != nil {
		log.Printf("Subscribe error : %v", err)
		return err
	}

	log.Printf("Subscribe response : %v", response)
	return nil
}
  • 在main方法中這樣呼叫
func main() {
	err := Subsciribe(TEST_TOPIC, TEST_GROUP, CONSUMER_NAME)
	if err != nil {
		fmt.Printf("err : %v\n", err)
	}
}

編寫程式碼驗證功能:拉取訊息

  • 以下是拉取訊息的程式碼
// 拉取訊息
func Poll(consumerGroup string, consumerName string) error {
	// ctx context.Context, groupid string, name string, localVarOptionals *PollOpts
	recordList, response, err := client.ConsumersApi.Poll(context.Background(), consumerGroup, consumerName, nil)
	if err != nil {
		log.Printf("Poll error : %v", err)
		return err
	}

	log.Printf("Poll response : %v", response)
	fmt.Printf("recordList: %v\n", recordList)
	return nil
}
  • main方法如下
func main() {
	Poll(TEST_GROUP, CONSUMER_NAME)
}
  • 執行main方法,第一次拉取不到訊息,別擔心,這是正常的現象,按照官方的說法,拉取到的第一條訊息就是空的,這是因為拉取操作出觸發了rebalancing邏輯(rebalancing是kafka的概覽,是處理多個partition消費的操作),再次執行main方法,這下正常了,控制檯輸出如下
/private/var/folders/5v/p3bj9bzx2nd99y5l21nb1c080000gn/T/GoLand/___go_build_sdkdemo
2022/12/18 21:43:16 Poll response : &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[2301] Content-Type:[application/vnd.kafka.json.v2+json]] 0x140000e0340 2301 [] false false map[] 0x1400011a100 <nil>}
recordList: [{ 163468 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 163469 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 163470 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 163471 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 163472 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 163473 0 bridge-quickstart-topic message from go swagger SDK <nil>} { 162246 2 bridge-quickstart-topic message from go swagger SDK <nil>} { 162247 2 bridge-quickstart-topic message from go swagger SDK <nil>} { 162248 2 bridge-quickstart-topic message from go swagger SDK <nil>} { 162249 2 bridge-quickstart-topic message from go swagger SDK <nil>} { 162250 2 bridge-quickstart-topic message from go swagger SDK <nil>} { 163669 1 bridge-quickstart-topic message from go swagger SDK <nil>} { 163670 1 bridge-quickstart-topic message from go swagger SDK <nil>} { 163671 1 bridge-quickstart-topic message from go swagger SDK <nil>} { 163672 1 bridge-quickstart-topic message from go swagger SDK <nil>} { 163146 3 bridge-quickstart-topic message from go swagger SDK <nil>} { 163147 3 bridge-quickstart-topic message from go swagger SDK <nil>} { 163148 3 bridge-quickstart-topic message from go swagger SDK <nil>} { 163149 3 bridge-quickstart-topic message from go swagger SDK <nil>} { 163150 3 bridge-quickstart-topic message from go swagger SDK <nil>}]

Process finished with the exit code 0

編寫程式碼驗證功能:提交offset

  • 最後是提交offset的功能,這樣從訊息的傳送再到接收的整個流程都實現了api覆蓋,增加Offset方法
// 提交offset
func Offset(consumerGroup string, consumerName string) error {
	response, err := client.ConsumersApi.Commit(context.Background(),
		consumerGroup,
		consumerName, nil)

	if err != nil {
		log.Printf("Poll error : %v", err)
		return err
	}

	log.Printf("Offset response : %v", response)
	return nil
}

  • 呼叫很簡單
func main() {
	err := Offset(TEST_GROUP, CONSUMER_NAME)
	if err != nil {
		print(err)
	}
}

  • 執行結果如下,返回204,提交成功
/private/var/folders/5v/p3bj9bzx2nd99y5l21nb1c080000gn/T/GoLand/___go_build_sdkdemo
2022/12/18 22:07:38 Offset response : &{204 No Content 204 HTTP/1.1 1 1 map[] {} 0 [] false false map[] 0x1400011a100 <nil>}

Process finished with the exit code 0

java的問題

  • 從go版本的修改程度可以發現,基於openapiv2.json生成的sdk程式碼真的很難用,在go環境尚且如此,換成java環境就更難改了,雖然我也嘗試過將其改好,但是面對很多jar的時候還是無能為力,下圖是一個很難處理的地方,ApiClient並不支援application/vnd.kafka.v2+jsonapplication/vnd.kafka.json.v2+json,contentType改不成正常的,bridge後臺就會返回錯誤,所以最終我只能罵罵咧咧的放棄了
    在這裡插入圖片描述

有收穫嗎?

  • 面對這麼爛的SDK原始碼,一般人都不會在生產環境使用,但是個人覺得也不是一無是處,這裡小結一下收穫
  1. 瞭解了go版本swagger sdk原始碼的基本結構,和請求響應邏輯
  2. 知道了大眾工具也有出問題的時候
  3. strimzi到底測試過嗎,這個做CICD自動化應該可以做到吧,能進CNCF的專案,也是會出問題的...

歡迎關注部落格園:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...

相關文章