深入理解Kafka Connect:轉換器和序列化

AI前線導讀:Kafka Connect是一個簡單但功能強大的工具,可用於Kafka和其他系統之間的集成。人們對Kafka Connect最常見的誤解之一是它的轉換器。這篇文章將告訴我們如何正確地使用消息的序列化格式,以及如何在Kafka Connect連接器中對其進行標準化。

Kafka Connect是Apache Kafka的一部分,爲其他數據存儲和Kafka提供流式集成。對於數據工程師來說,他們只需要配置一下JSON文件就可以了。Kafka提供了一些可用於常見數據存儲的連接器,如JDBC、Elasticsearch、IBM MQ、S3和BigQuery,等等。

對於開發人員來說,Kafka Connect提供了豐富的API,如果有必要還可以開發其他連接器。除此之外,它還提供了用於配置和管理連接器的REST API。

Kafka Connect是一種模塊化組件,提供了一種非常強大的集成方法。一些關鍵組件包括:

  • 連接器——定義如何與數據存儲集成的JAR文件;
  • 轉換器——處理數據的序列化和反序列化;
  • 變換——可選的運行時消息操作。

人們對Kafka Connect最常見的誤解與數據的序列化有關。Kafka Connect使用轉換器處理數據序列化。接下來讓我們看看它們是如何工作的,並說明如何解決一些常見問題。

Kafka消息都是字節

Kafka消息被保存在主題中,每條消息就是一個鍵值對。當它們存儲在Kafka中時,鍵和值都只是字節。Kafka因此可以適用於各種場景,但這也意味着開發人員需要決定如何序列化數據。

在配置Kafka Connect時,序列化格式是最關鍵的配置選項之一。你需要確保從主題讀取數據時使用的序列化格式與寫入主題的序列化格式相同,否則就會出現混亂和錯誤!

image

序列化格式有很多種,常見的包括:

  • JSON;
  • Avro;
  • Protobuf;
  • 字符串分隔(如CSV)。

選擇序列化格式

選擇序列化格式的一些指導原則:

  • schema。很多時候,你的數據都有對應的schema。你可能不喜歡,但作爲開發人員,你有責任保留和傳播schema。schema爲服務之間提供了一種契約。某些消息格式(例如Avro和Protobuf)具有強大的schema支持,而其他消息格式支持較少(JSON)或根本沒有(CVS)。
  • 生態系統兼容性。Avro是Confluent平臺的一等公民,擁有來自Confluent Schema Registry、Kafka Connect、KSQL的原生支持。另一方面,Protobuf依賴社區爲部分功能提供支持。
  • 消息大小。JSON是純文本的,並且依賴了Kafka本身的壓縮機制,Avro和Protobuf都是二進制格式,序列化的消息體積更小。
  • 語言支持。Avro在Java領域得到了強大的支持,但如果你的公司不是基於Java的,那麼可能會覺得它不太好用。

如果目標系統使用JSON,Kafka主題也必須使用JSON嗎?

完全不需要這樣。從數據源讀取數據或將數據寫入外部數據存儲的格式不需要與Kafka消息的序列化格式一樣。

Kafka Connect中的連接器負責從源數據存儲(例如數據庫)獲取數據,並以數據內部表示將數據傳給轉換器。然後,Kafka Connect的轉換器將這些源數據對象序列化到主題上。

在使用Kafka Connect作爲接收器時剛好相反——轉換器將來自主題的數據反序列化爲內部表示,傳給連接器,以便能夠使用特定於目標的適當方法將數據寫入目標數據存儲。

也就是說,主題數據可以是Avro格式,當你將數據寫入HDFS時,指定接收器的連接器使用HDFS支持的格式即可。

配置轉換器

Kafka Connect默認使用了worker級別的轉換器配置,連接器可以對其進行覆蓋。由於在整個管道中使用相同的序列化格式通常會更好,所以一般只需要在worker級別設置轉換器,而不需要在連接器中指定。但你可能需要從別人的主題拉取數據,而他們使了用不同的序列化格式——對於這種情況,你需要在連接器配置中設置轉換器。即使你在連接器的配置中進行了覆蓋,它仍然是執行實際任務的轉換器。

好的連接器一般不會序列化或反序列化存儲在Kafka中的消息,它會讓轉換器完成這項工作。

image

請記住,Kafka消息是鍵值對字節,你需要使用key.converter和value.converter爲鍵和值指定轉換器。在某些情況下,你可以爲鍵和值使用不同的轉換器。

image

這是使用String轉換器的一個示例。

"key.converter": "org.apache.kafka.connect.storage.StringConverter",

有些轉換器有一些額外的配置。對於Avro,你需要指定Schema Registry。對於JSON,你需要指定是否希望Kafka Connect將schema嵌入到JSON消息中。在指定特定於轉換器的配置時,請始終使用key.converter.或value.converter.前綴。例如,要將Avro用於消息載荷,你需要指定以下內容:

"value.converter": "io.confluent.connect.avro.AvroConverter",
"value.converter.schema.registry.url": "http://schema-registry:8081",

常見的轉換器包括:

  • Avro——來自Confluent的開源項目
io.confluent.connect.avro.AvroConverter
  • String——Apache Kafka的一部分
org.apache.kafka.connect.storage.StringConverter
  • JSON——Apache Kafka的一部分
org.apache.kafka.connect.json.JsonConverter
  • ByteArray——Apache Kafka的一部分
org.apache.kafka.connect.converters.ByteArrayConverter
  • Protobuf——來自社區的開源項目
com.blueapron.connect.protobuf.ProtobufConverter

JSON和schema

雖然JSON默認不支持嵌入schema,但Kafka Connect提供了一種可以將schema嵌入到消息中的特定JSON格式。由於schema被包含在消息中,因此生成的消息大小可能會變大。

如果你正在設置Kafka Connect源,並希望Kafka Connect在寫入Kafka消息包含schema,可以這樣:

value.converter=org.apache.kafka.connect.json.JsonConverter
value.converter.schemas.enable=true

生成的Kafka消息看起來像下面這樣,其中包含schema和payload節點元素:

{
  "schema": {
    "type": "struct",
    "fields": [
      {
        "type": "int64",
        "optional": false,
        "field": "registertime"
      },
      {
        "type": "string",
        "optional": false,
        "field": "userid"
      },
      {
        "type": "string",
        "optional": false,
        "field": "regionid"
      },
      {
        "type": "string",
        "optional": false,
        "field": "gender"
      }
    ],
    "optional": false,
    "name": "ksql.users"
  },
  "payload": {
    "registertime": 1493819497170,
    "userid": "User_1",
    "regionid": "Region_5",
    "gender": "MALE"
  }
}

請注意消息的大小,消息由playload和schema組成。每條消息中都會重複這些數據,這也就是爲什麼說Avro這樣的格式會更好,因爲它的schema是單獨存儲的,消息中只包含載荷(並進行了壓縮)。

如果你正在使用Kafka Connect消費Kafka主題中的JSON數據,那麼就需要知道數據是否包含了schema。如果包含了,並且它的格式與上述的格式相同,那麼你可以這樣設置:

value.converter=org.apache.kafka.connect.json.JsonConverter
value.converter.schemas.enable=true

不過,如果你正在消費的JSON數據如果沒有schema加payload這樣的結構,例如:

{
  "registertime": 1489869013625,
  "userid": "User_1",
  "regionid": "Region_2",
  "gender": "OTHER"
}

那麼你必須通過設置schemas.enable = false告訴Kafka Connect不要查找schema:

value.converter=org.apache.kafka.connect.json.JsonConverter
value.converter.schemas.enable=false

和之前一樣,轉換器配置選項(這裏是schemas.enable)需要使用前綴key.converter或value.converter。

常見錯誤

如果你錯誤地配置了轉換器,將會遇到以下的一些常見錯誤。這些消息將顯示在你爲Kafka Connect配置的接收器中,因爲你試圖在接收器中反序列化Kafka消息。這些錯誤會導致連接器失敗,主要錯誤消息如下所示:

ERROR WorkerSinkTask{id=sink-file-users-json-noschema-01-0} Task threw an uncaught and unrecoverable exception (org.apache.kafka.connect.runtime.WorkerTask)
org.apache.kafka.connect.errors.ConnectException: Tolerance exceeded in error handler
   at org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator. execAndHandleError(RetryWithToleranceOperator.java:178)
   at org.apache.kafka.connect.runtime.errors.RetryWithToleranceOperator.execute (RetryWithToleranceOperator.java:104)

在錯誤消息的後面,你將看到堆棧信息,描述了發生錯誤的原因。請注意,對於連接器中的任何致命錯誤,都會拋出上述異常,因此你可能會看到與序列化無關的錯誤。要快速查看錯誤配置可能會導致的錯誤,請參考下表:

image

問題:使用JsonConverter讀取非JSON數據

如果你的源主題上有非JSON數據,但嘗試使用JsonConverter讀取它,你將看到:

org.apache.kafka.connect.errors.DataException: Converting byte[] to Kafka Connect data failed due to serialization error:
…
org.apache.kafka.common.errors.SerializationException: java.io.CharConversionException: Invalid UTF-32 character 0x1cfa7e2 (above 0x0010ffff) at char #1, byte #7)

這有可能是因爲源主題使用了Avro或其他格式。

解決方案:如果數據是Avro格式的,那麼將Kafka Connect接收器的配置改爲:

"value.converter": "io.confluent.connect.avro.AvroConverter",
"value.converter.schema.registry.url": "http://schema-registry:8081",

或者,如果主題數據是通過Kafka Connect填充的,那麼你也可以這麼做,讓上游源也發送JSON數據:

"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",

問題:使用AvroConverter讀取非Avro數據

這可能是我在Confluent Community郵件組和Slack組等地方經常看到的錯誤。當你嘗試使用Avro轉換器從非Avro主題讀取數據時,就會發生這種情況。這包括使用Avro序列化器而不是Confluent Schema Registry的Avro序列化器(它有自己的格式)寫入的數據。

org.apache.kafka.connect.errors.DataException: my-topic-name
at io.confluent.connect.avro.AvroConverter.toConnectData(AvroConverter.java:97)
…
org.apache.kafka.common.errors.SerializationException: Error deserializing Avro message for id -1
org.apache.kafka.common.errors.SerializationException: Unknown magic byte!

解決方案:檢查源主題的序列化格式,修改Kafka Connect接收器連接器,讓它使用正確的轉換器,或將上游格式切換爲Avro。如果上游主題是通過Kafka Connect填充的,則可以按如下方式配置源連接器的轉換器:

"value.converter": "io.confluent.connect.avro.AvroConverter",
"value.converter.schema.registry.url": "http://schema-registry:8081",

問題:沒有使用預期的schema/payload結構讀取JSON消息

如前所述,Kafka Connect支持包含載荷和schema的JSON消息。如果你嘗試讀取不包含這種結構的JSON數據,你將收到這個錯誤:

org.apache.kafka.connect.errors.DataException: JsonConverter with schemas.enable requires "schema" and "payload" fields and may not contain additional fields. If you are trying to deserialize plain JSON data, set schemas.enable=false in your converter configuration.

需要說明的是,當schemas.enable=true時,唯一有效的JSON結構需要包含schema和payload這兩個頂級元素(如上所示)。

如果你只有簡單的JSON數據,則應將連接器的配置改爲:

"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "false",

如果要在數據中包含schema,可以使用Avro(推薦),也可以修改上游的Kafka Connect配置,讓它在消息中包含schema:

"value.converter": "org.apache.kafka.connect.json.JsonConverter",
"value.converter.schemas.enable": "true",

故障排除技巧

查看Kafka Connect日誌

要在Kafka Connect中查找錯誤日誌,你需要找到Kafka Connect工作程序的輸出。這個位置取決於你是如何啓動Kafka Connect的。有幾種方法可用於安裝Kafka Connect,包括Docker、Confluent CLI、systemd和手動下載壓縮包。你可以這樣查找日誌的位置:

  • Docker:docker logs container_name;
  • Confluent CLI:confluent log connect;
  • systemd:日誌文件在/var/log/confluent/kafka-connect;
  • 其他:默認情況下,Kafka Connect將其輸出發送到stdout,因此你可以在啓動Kafka Connect的終端中找到它們。

查看Kafka Connect配置文件

  • Docker——設置環境變量,例如在Docker Compose中:
CONNECT_KEY_CONVERTER: io.confluent.connect.avro.AvroConverter
CONNECT_KEY_CONVERTER_SCHEMA_REGISTRY_URL: 'http://schema-registry:8081'
CONNECT_VALUE_CONVERTER: io.confluent.connect.avro.AvroConverter
CONNECT_VALUE_CONVERTER_SCHEMA_REGISTRY_URL: 'http://schema-registry:8081'
  • Confluent CLI——使用配置文件etc/schema-registry/connect-avro-distributed.properties;
  • systemd(deb/rpm)——使用配置文件/etc/kafka/connect-distributed.properties;
  • 其他——在啓動Kafka Connect時指定工作程序的屬性文件,例如:
$ cd confluent-5.0.0
$ ./bin/connect-distributed ./etc/kafka/connect-distributed.properties

檢查Kafka主題

假設我們遇到了上述當中的一個錯誤,並且想要解決Kafka Connect接收器無法從主題讀取數據的問題。

我們需要檢查正在被讀取的數據,並確保它使用了正確的序列化格式。另外,所有消息都必須使用這種格式,所以不要假設你現在正在以正確的格式向主題發送消息就不會出問題。Kafka Connect和其他消費者也會從主題上讀取已有的消息。

下面,我將使用命令行進行故障排除,當然也可以使用其他的一些工具:

  • Confluent Control Center提供了可視化檢查主題內容的功能;
  • KSQL的PRINT命令將主題的內容打印到控制檯;
  • Confluent CLI工具提供了consume命令,可用於讀取字符串和Avro數據。

如果你的數據是字符串或JSON格式

你可以使用控制檯工具,包括kafkacat和kafka-console-consumer。我個人的偏好是使用kafkacat:

$ kafkacat -b localhost:9092 -t users-json-noschema -C -c1
{"registertime":1493356576434,"userid":"User_8","regionid":"Region_2","gender":"MALE"}

你也可以使用jq驗證和格式化JSON:

$ kafkacat -b localhost:9092 -t users-json-noschema -C -c1|jq '.'
{
  "registertime": 1493356576434,
  "userid": "User_8",
  "regionid": "Region_2",
  "gender": "MALE"
}

如果你得到一些“奇怪的”字符,你查看的很可能是二進制數據,這些數據是通過Avro或Protobuf寫入的:

$ kafkacat -b localhost:9092 -t users-avro -C -c1
ڝ���VUser_9Region_MALE

如果你的數據是Avro格式

你應該使用專爲讀取和反序列化Avro數據而設計的控制檯工具。我使用的是kafka-avro-console-consumer。確保指定了正確的Schema Registry URL:

$ kafka-avro-console-consumer --bootstrap-server localhost:9092 \
                              --property schema.registry.url=http://localhost:8081 \
                              --topic users-avro \
                              --from-beginning --max-messages 1
{"registertime":1505213905022,"userid":"User_5","regionid":"Region_4","gender":"FEMALE"}

和之前一樣,如果要格式化,可以使用jq:

$ kafka-avro-console-consumer --bootstrap-server localhost:9092 \
                              --property schema.registry.url=http://localhost:8081 \
                              --topic users-avro \
                              --from-beginning --max-messages 1 | \
                              jq '.'
{
  "registertime": 1505213905022,
  "userid": "User_5",
  "regionid": "Region_4",
  "gender": "FEMALE"
}

內部轉換器

在分佈式模式下運行時,Kafka Connect使用Kafka來存儲有關其操作的元數據,包括連接器配置、偏移量等。

可以通過internal.key.converter/internal.value.converter讓這些Kafka使用不同的轉換器。不過這些設置只在內部使用,實際上從Apache Kafka 2.0開始就已被棄用。你不應該更改這些配置,從Apache Kafka 2.0版開始,如果你這麼做了將會收到警告。

將schema應用於沒有schema的消息

很多時候,Kafka Connect會從已經存在schema的地方引入數據,並使用合適的序列化格式(例如Avro)來保留這些schema。然後,這些數據的所有下游用戶都可以使用這些schema。但如果沒有提供顯式的schema該怎麼辦?

或許你正在使用FileSourceConnector從普通文件中讀取數據(不建議用於生產環境中,但可用於PoC),或者正在使用REST連接器從REST端點提取數據。由於它們都沒有提供schema,因此你需要聲明它。

有時候你只想傳遞你從源讀取的字節,並將它們保存在一個主題上。但大多數情況下,你需要schema來使用這些數據。在攝取時應用一次schema,而不是將問題推到每個消費者,這纔是一種更好的處理方式。

你可以編寫自己的Kafka Streams應用程序,將schema應用於Kafka主題中的數據上,當然你也可以使用KSQL。下面讓我們來看一下將schema應用於某些CSV數據的簡單示例。

假設我們有一個Kafka主題testdata-csv,保存着一些CSV數據,看起來像這樣:

$ kafkacat -b localhost:9092 -t testdata-csv -C
1,Rick Astley,Never Gonna Give You Up
2,Johnny Cash,Ring of Fire

我們可以猜測它有三個字段,可能是:

  • ID
  • Artist
  • Song

如果我們將數據保留在這樣的主題中,那麼任何想要使用這些數據的應用程序——無論是Kafka Connect接收器還是自定義的Kafka應用程序——每次都需要都猜測它們的schema是什麼。或者,每個消費應用程序的開發人員都需要向提供數據的團隊確認schema是否發生變更。正如Kafka可以解耦系統一樣,這種schema依賴讓團隊之間也有了硬性耦合,這並不是一件好事。

因此,我們要做的是使用KSQL將schema應用於數據上,並使用一個新的派生主題來保存schema。這樣你就可以通過KSQL檢查主題數據:

ksql> PRINT 'testdata-csv' FROM BEGINNING;
Format:STRING
11/6/18 2:41:23 PM UTC , NULL , 1,Rick Astley,Never Gonna Give You Up
11/6/18 2:41:23 PM UTC , NULL , 2,Johnny Cash,Ring of Fire

前兩個字段(11/6/18 2:41:23 PM UTC和NULL)分別是Kafka消息的時間戳和鍵。其餘字段來自CSV文件。現在讓我們用KSQL註冊這個主題並聲明schema:

ksql> CREATE STREAM TESTDATA_CSV (ID INT, ARTIST VARCHAR, SONG VARCHAR) \
WITH (KAFKA_TOPIC='testdata-csv', VALUE_FORMAT='DELIMITED');

Message
----------------
Stream created
----------------

可以看到,KSQL現在有一個數據流schema:

ksql> DESCRIBE TESTDATA_CSV;

Name                 : TESTDATA_CSV
 Field   | Type
-------------------------------------
 ROWTIME | BIGINT (system)
 ROWKEY  | VARCHAR(STRING) (system)
 ID      | INTEGER
 ARTIST  | VARCHAR(STRING)
 SONG    | VARCHAR(STRING)
-------------------------------------
For runtime statistics and query details run: DESCRIBE EXTENDED <Stream,Table>;

可以通過查詢KSQL流來檢查數據是否符合預期。請注意,這個時候我們只是作爲現有Kafka主題的消費者——並沒有更改或複製任何數據。

ksql> SET 'auto.offset.reset' = 'earliest';
Successfully changed local property 'auto.offset.reset' from 'null' to 'earliest'
ksql> SELECT ID, ARTIST, SONG FROM TESTDATA_CSV;
1 | Rick Astley | Never Gonna Give You Up
2 | Johnny Cash | Ring of Fire

最後,創建一個新的Kafka主題,使用帶有schema的數據進行填充。KSQL查詢是持續的,因此除了將現有的數據從源主題發送到目標主題之外,KSQL還將向目標主題發送未來將生成的數據。

ksql> CREATE STREAM TESTDATA WITH (VALUE_FORMAT='AVRO') AS SELECT * FROM TESTDATA_CSV;

Message
----------------------------
Stream created and running
----------------------------

使用Avro控制檯消費者驗證數據:

$ kafka-avro-console-consumer --bootstrap-server localhost:9092 \
                                --property schema.registry.url=http://localhost:8081 \
                                --topic TESTDATA \
                                --from-beginning | \
                                jq '.'
{
  "ID": {
    "int": 1
},
  "ARTIST": {
    "string": "Rick Astley"
},
  "SONG": {
    "string": "Never Gonna Give You Up"
  }
}
[…]

你甚至可以在Schema Registry中查看已註冊的schema:

$ curl -s http://localhost:8081/subjects/TESTDATA-value/versions/latest|jq '.schema|fromjson'
{
  "type": "record",
  "name": "KsqlDataSourceSchema",
  "namespace": "io.confluent.ksql.avro_schemas",
  "fields": [
    {
      "name": "ID",
      "type": [
        "null",
        "int"
      ],
      "default": null
    },
    {
      "name": "ARTIST",
      "type": [
        "null",
        "string"
      ],
      "default": null
    },
    {
      "name": "SONG",
      "type": [
        "null",
        "string"
      ],
      "default": null
    }
  ]
}

寫入原始主題(testdata-csv)的任何新消息都由KSQL自動處理,並以Avro格式寫入新的TESTDATA主題。現在,任何想要使用這些數據的應用程序或團隊都可以使用TESTDATA主題。你還可以更改主題的分區數、分區鍵和複製係數。

英文原文:https://www.confluent.io/blog/kafka-connect-deep-dive-converters-serialization-explained

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章