《Debezium系列》Debezium連接器使用Avro序列化

官方文章參考:

https://debezium.io/documentation/reference/1.1/configuration/avro.html

書籍參考: kafka權威指南

 

1. 起因

對於CDC產生的事件信息來說,debezium給出的消息內容過多,其中對於元數據的結構描述長並且每條消息都需要參夾發送。剛好官方文檔提供除了String、Json以外的一種序列化工具:Avro。

並使用Confluent提供的Schema-Registry進行Avro的相關配置。

它,短小精悍;它,二進制大小小;它,schema修改更新元數據,不會報錯;它:我要打十個。

 

2. Avro簡介

Avro爲Apache項目,是一個數據序列化系統。序列化格式。(官網展示相關特性如下)

Avro provides:

  • Rich data structures.
  • A compact, fast, binary data format.
  • A container file, to store persistent data.
  • Remote procedure call (RPC).
  • Simple integration with dynamic languages. Code generation is not required to read or write data files nor to use or implement RPC protocols. Code generation as an optional optimization, only worth implementing for statically typed languages.

 

權威指南中有:

其中Avro重要的是,就是寫消息的應用程序使用了新的schema(例如更改了字段類型或者新增字段),負責讀的應用程序可以繼續處理消息而不用做改動。

例如有一個類:

public class Customer {
    private int id;
    private String name;
    private String faxNumber;
}

// 某天它變成了

public class Customer {
    private int id;
    private String name;
    private String email;
}

在應用程序升級之前,它們會調用類似 getName()、getId() 和 getFaxNumber() 這樣的方法。如果碰到使用新 schema 構建的消息,getName() 和 getId() 方法仍然能夠正常返回,但 getFaxNumber() 方法會返回 null,因爲消息裏不包含傳真號碼。

在應用程序升級之後,getEmail() 方法取代了 getFaxNumber() 方法。如果碰到一個使用舊 schema 構建的消息,那麼 getEmail() 方法會返回 null,因爲舊消息不包含郵件地址。

我們修改了消息的 schema,但並沒有更新所有負責讀取數據的應用程序,而這樣仍然不會出現異常或阻斷性錯誤,也不需要對現有數據進行大幅更新。

 

其中最重要的是:

它通過schema來定義數據,而schema是用json來描述的。數據會被序列化成二進制格式進行傳輸,Avro在讀寫文件的時候需要用到schema,schema一般會被內嵌到數據文件中。

簡單示例:

對於kafka來說,不可能在每條消息中嵌套schema消息,這個會增加數據大小,傳輸的時候增大開銷。

所以這個時候就需要一箇中間組件,註冊表(schema-registry),在生產消息的時候相關的schema存儲在註冊表中並給與一個id可以尋找到該schema(該註冊表不屬於kafka)。然後在讀取該消息的時候去註冊表拿取schema信息。

所以這就很適合kafka這樣的消息系統。

 

3. 在Kafka中的Avro簡單例子

道理我都懂,要不實踐一下?

 

所以通過下面這個生產者例子瞭解下Avro的實際使用。

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;

public class AvroProducer {
    public static void main(String[] args) {
        Properties pros = new Properties();
        pros.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        // 選擇Confluent Avro序列化
        pros.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "io.confluent.kafka.serializers.KafkaAvroSerializer");
        pros.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "io.confluent.kafka.serializers.KafkaAvroSerializer");
        // schema-registry地址
        pros.put("schema.registry.url", "http://schemaUrl:8081");
        // schema info
        String schemaString = "{\"namespace\":\"customerManagement.avro\",\"type\":\"record\"," +
                "\"name\" : \"Customer\", \"fields\" : [{\"name\":\"id\",\"type\":\"string\"}," +
                "{\"name\": \"name\", \"fields\" : \"string\"}" +
                "{\"name\" : \"email\", \"type\" : [\"null\",\"string\"], \"default\" : \"null\"}]}";
        Producer<String, GenericRecord> producer = new KafkaProducer<String, GenericRecord>(pros);
        // 構建Schema
        Schema.Parser parser = new Schema.Parser();
        Schema schema = parser.parse(schemaString);
        // 模擬消息發送
        for (int i = 0; i < 10; i++) {
            GenericRecord customer = new GenericData.Record(schema);
            String name = "customer" + i;
            // 此處會進行映射
            customer.put("id", name);
            customer.put("name", "MyName");
            customer.put("email", "[email protected]");
            // 指定發送的topic、key、value
            ProducerRecord<String, GenericRecord> data = new ProducerRecord<String, GenericRecord>
                    ("customerContacts", name, customer);
            // 發送
            producer.send(data);
        }
    }
}

4. 爲Debezium連接器配置Confluent的Avro Schema Registry

其實這個標題起的不太好,因爲Confluent的Scheam Registry並不只是Avro專用,也支持其他序列化,它只是一個註冊表而已。

官方傳送---https://docs.confluent.io/current/schema-registry/index.html

 

其Schema的架構如下:

 

4.1 安裝Confluent配置Schema

例如在centos下安裝:

https://docs.confluent.io/5.4.1/installation/installing_cp/rhel-centos.html#systemd-rhel-centos-install

這裏推薦安裝社區版本就夠用了。

修改該目錄下的配置文件:

啓動schema

[root@host01 schema-registry]# schema-registry-start schema-registry.properties

4.2. Connect全局配置或者提交connector時進行配置

connect.properties

key.converter=io.confluent.connect.avro.AvroConverter
key.converter.schema.registry.url=http://localhost:8081
value.converter=io.confluent.connect.avro.AvroConverter
value.converter.schema.registry.url=http://localhost:8081

# 對於debezium來說,此處注意全部配置avro序列化時,不建議配置爲avro
# 需要配置kafka內部轉換爲json

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

# 此處需要配置confluent的jar包目錄,例如/usr/local/tools/confluent-5.4.1/share/java下面。
# 若是不需要那麼多的jar包,也可以去官網下載單獨的包
plugin=/usr/local/tools/confluent-5.4.1/share/java

Confluent的Avro序列化相關jar包:

https://www.confluent.io/hub/confluentinc/kafka-connect-avro-converter

下載完解壓到connect指定的plugin目錄下。

關於內部轉換的官方解釋:

或者進行connector提交時進行添加如上屬性

"key.converter"="io.confluent.connect.avro.AvroConverter"
"key.converter.schema.registry.url"="http://localhost:8081"
"value.converter"="io.confluent.connect.avro.AvroConverter"
"value.converter.schema.registry.url"="http://localhost:8081"

 

啓動connect-distributed

或者

提交connector

若無Exception拋出則配置成功。

 

5. 遇到的問題

1. 進行Avro序列化提交的時候無法找到相關類

我將jar包放進plugin文件夾後,啓動各種connector和反覆啓動connect-distributed。

都沒有將其包引入,一直NoClassDefFoundError,我都快懷疑人生了。

java.lang.NoClassDefFoundError: io/confluent/connect/avro/AvroConverterConfig

這種可能是jvm找不到該包classpath地址,導致未加載進去。執行下面語句解決。

export CLASSPATH=/usr/local/tools/kafka/kafka_2.12-2.4.0/plugin/*

2. 消費的數據不正確,例如Decimal格式數據未完全轉換過來

插入數據爲:

INSERT INTO debeziumdb.data_type_test
(id, varchar_data, bigint_data, float_data, double_date, decimal_data, date_data, time_data, datetime_data, timestamp_data)
VALUES(1, 'www', 1, 1.10000002, 1.11, 5000.36, '2020-02-02', '12:12:10', '2020-02-02 12:12:10.0', NULL);

使用Confluent的Avro序列化消費結果爲:

{"before": null, "after": {"id": 1, "varchar_data": "www", "bigint_data": 1, "float_data": 1.100000023841858, "double_date": 1.11, "decimal_data": "\u0007¡D", "date_data": 18294, "time_data": 43930000000, "datetime_data": 1580645530000, "timestamp_data": "1970-01-01T00:00:00Z"}, "source": {"version": "1.1.0.Final", "connector": "mysql", "name": "dbz.mysql", "ts_ms": 0, "snapshot": "last", "db": "debeziumdb", "table": "data_type_test", "server_id": 0, "gtid": null, "file": "MS-SJNKFHHAUOQV-bin.000003", "pos": 16244421, "row": 0, "thread": null, "query": null}, "op": "c", "ts_ms": 1588060753486, "transaction": null}

該問題未解決,正在解決中。嘗試自己手動再進行解析。

 

6. 相關想法

大家都知道kafka是一個分佈式架構,對於大量的請求可以分發緩解壓力。

但是對於schema來說,假如我只啓動了一個schema,之後的所有生產者都會將其schema註冊到註冊表中,如果壓力過大會不會崩掉。

如果我啓動多個schema,配置如何配置,是否會自動負載均衡?

這些都有待考究(可能Confluent已經考慮好了,我沒發現而已)。

        若是瞭解清楚了,會再出一篇說明。

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