Databricks (Spark) でシリアライズしたデータを Confluent 上で文字化けする事象について


## 概要

Databricks (Spark) でシリアライズしたデータを Confluent に書き込むと、Confluent のコンソール上で Value 列が文字化けして見えてしまう事象について解説します。これは想定外の動作ではなく、Confluent の Wire Header 仕様が影響して起こる現象です。本記事では、事象の説明・再現方法・対応方法を順を追って紹介します。  
なお、ここで紹介する対応方法は動作確認や仕様の理解を目的とした簡易的なものです。実際の運用では **Confluent Schema Registry** を利用する方法を強くおすすめします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e9c890cd-6d62-eca4-bee6-1171970d95fe.png)

## 事象について

Databricks 上でシリアライズしたデータを Confluent に書き込むと、次のように Value 列が文字化けして表示されることがあります。

| key      | Value                                 |
| -------- | ------------------------------------- |
| "User_1" | "�����W\fUser_1\u0010Region_1\nOTHER" |
| "User_2" | "�³ۡX\fUser_2\u0010Region_2\nOTHER"    |
| "User_3" | "���W\fUser_3\u0010Region_3\fFEMALE"  |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e9c890cd-6d62-eca4-bee6-1171970d95fe.png)

しかし、Databricks から同じトピックを読み込んだ場合は想定通りにデータが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4e0d5687-9a92-3245-c38e-08278837bada.png)

これは Confluent 側で Wire Header が設定されていないため、Confluent の Web コンソール上で文字化けが起きているだけです。Wire Header を付加して書き込むことで、コンソール上でも文字化けせずに表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/c0ac5a1b-fd88-e493-67d8-3336d6e094c5.png)

> 引用元: [Formats, Serializers, and Deserializers for Schema Registry on Confluent Cloud | Confluent Documentation](https://docs.confluent.io/cloud/current/sr/fundamentals/serdes-develop/index.html#wire-format)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/73f80944-669c-3c62-7fa3-2391838fe49d.png)


## 事前準備

### 1. Confluent で API キーを取得


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/1eb6d6dd-21bd-db9f-2ce3-1c41c3e9dfbd.png)

### 2. Databricks 上で Confluent への認証情報をセット

```python
# Confluent への接続情報を設定
bootstrap_servers = "pkc-921jm.us-east-2.aws.confluent.cloud:9092"
sasl_username = "NYQNA56RYSHSESZK"
sasl_password = "g0AwC7hwasWL49Vn5MdBWrZeaQBFpv3DH0QcTWj7Ovh/HE1R2dWUssi0VxBc6JqW"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/565914ff-30b9-4823-2296-1c85f0e69105.png)

### 3. 検証用に Databricks のオブジェクトを作成

```sql
%sql
CREATE CATALOG IF NOT EXISTS confluent_test;
CREATE SCHEMA IF NOT EXISTS confluent_test.test;
```

```python
src_table_name = "confluent_test.test.src_table"
checkpoint_volume_name = "confluent_test.test.checkpoint_01"
checkpoint_volume_dir = f"/Volumes/confluent_test/test/checkpoint_01"
```

```python
spark.sql(f"CREATE TABLE IF NOT EXISTS {src_table_name}")
spark.sql(f"CREATE VOLUME IF NOT EXISTS {checkpoint_volume_name}")
dbutils.fs.rm(checkpoint_volume_dir, True)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/92ba4b1a-f78d-ef50-a4dc-12e4c9c721cc.png)

### 4. ソースとなる Databricks のテーブルにデータを書き込み

```python
data = [
    ("User_1", 1507214855421, "Region_1", "OTHER"),
    ("User_2", 1516353515688, "Region_2", "OTHER"),
    ("User_3", 1511384261793, "Region_3", "FEMALE")
]

columns = ["userid", "registertime", "regionid", "gender"]
df = spark.createDataFrame(data, columns)

df.write.format("delta").mode("overwrite").saveAsTable(src_table_name)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/614909c5-9b49-03cf-b555-f72c1ea51923.png)

## 文字化け事象の再現方法

### 1. Confluent で Topic を作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/77868832-ecd9-4d08-bd65-311105c767e0.png)

### 2. Databricks 上で Topic とデータスキーマを定義

```python
topic_name_without_schema = "topic_name_without_schema"
schema = """
{
  "connect.name": "ksql.users",
  "fields": [
    {
      "name": "registertime",
      "type": "long"
    },
    {
      "name": "userid",
      "type": "string"
    },
    {
      "name": "regionid",
      "type": "string"
    },
    {
      "name": "gender",
      "type": "string"
    }
  ],
  "name": "users",
  "namespace": "ksql",
  "type": "record"
}
"""
```

### 3. Databricks で Confluent に書き込むデータセットを定義

```python
from pyspark.sql.functions import col, struct
from pyspark.sql.avro.functions import to_avro

src_df = spark.readStream.table(src_table_name)

# key 列を設定
src_df = src_df.withColumn("key", col("userid"))

# value 列（Avro形式にシリアライズ）
src_df = src_df.withColumn("value", struct("registertime", "userid", "regionid", "gender"))
src_df = src_df.withColumn("value", to_avro(col("value"), schema))

# key と value だけを出力カラムに
src_df = src_df.select("key", "value")
```

### 4. Confluent に書き込み

```python
(
    src_df.writeStream.format("kafka")
    .option("kafka.bootstrap.servers", bootstrap_servers)
    .option("kafka.security.protocol", "SASL_SSL")
    .option(
        "kafka.sasl.jaas.config",
        f'kafkashaded.org.apache.kafka.common.security.plain.PlainLoginModule required username="{sasl_username}" password="{sasl_password}";',
    )
    .option("kafka.sasl.mechanism", "PLAIN")
    .option("topic", topic_name_without_schema)
    .option("checkpointLocation", checkpoint_dir)
    .trigger(availableNow=True)
    .start()
)
```

### 5. Confluent 上で文字化けを確認

| key      | Value                                 |
| -------- | ------------------------------------- |
| "User_1" | "�����W\fUser_1\u0010Region_1\nOTHER" |
| "User_2" | "�³ۡX\fUser_2\u0010Region_2\nOTHER"    |
| "User_3" | "���W\fUser_3\u0010Region_3\fFEMALE"  |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e9c890cd-6d62-eca4-bee6-1171970d95fe.png)

### 6. Databricks 上で Confluent からデータを取得し、文字化けしていないことを確認

```python
from pyspark.sql.functions import col
from pyspark.sql.avro.functions import from_avro

# Confluent から読み込み
df = (
    spark.readStream.format("kafka")
    .option("kafka.bootstrap.servers", bootstrap_servers)
    .option("kafka.security.protocol", "SASL_SSL")
    .option(
        "kafka.sasl.jaas.config",
        f'kafkashaded.org.apache.kafka.common.security.plain.PlainLoginModule required username="{sasl_username}" password="{sasl_password}";',
    )
    .option("kafka.sasl.mechanism", "PLAIN")
    .option("subscribe", topic_name_without_schema)
    .option("startingOffsets", "earliest")
    .load()
)

# Key をデシリアライズ
key_des_df = df.withColumn("key", col("Key").cast("string"))

# Value をデシリアライズ
decoded_df = key_des_df.withColumn(
    "decoded_value",
    from_avro(col("value"), schema)
)

# 展開したカラムを表示
tgt_cols = [
    "key",
    "decoded_value.*",
    "topic",
    "partition",
    "offset",
    "timestamp",
    "timestampType"
]
src_df = decoded_df.select(tgt_cols)
```

```python
src_df.display()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2b1ca147-c701-5c0f-9893-5122fe1478e8.png)

## 文字化け事象への対応方法

### 1. Confluent で Topic をスキーマ指定して作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/28b5b7cc-a503-df2e-a777-34c49284f3d3.png)

### 2. Data Contract タブでスキーマを追加

```text
{
  "connect.name": "ksql.users",
  "fields": [
    {
      "name": "registertime",
      "type": "long"
    },
    {
      "name": "userid",
      "type": "string"
    },
    {
      "name": "regionid",
      "type": "string"
    },
    {
      "name": "gender",
      "type": "string"
    }
  ],
  "name": "users",
  "namespace": "ksql",
  "type": "record"
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9ff63d71-8ee2-e124-4979-c83f4bed710a.png)

### 3. Confluent 上で Schema ID を取得（例：`100005`）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3cf903cf-bbf4-87e9-3c90-2aa39c8cba11.png)

### 4. Databricks 上で Schema ID を変数にセット

```python
schema_id_num = 100005
```

### 5. Databricks 上で Topic とスキーマを変数にセット

```python
topic_name_with_schema = "topic_name_with_schema"
schema = """
{
  "connect.name": "ksql.users",
  "fields": [
    {
      "name": "registertime",
      "type": "long"
    },
    {
      "name": "userid",
      "type": "string"
    },
    {
      "name": "regionid",
      "type": "string"
    },
    {
      "name": "gender",
      "type": "string"
    }
  ],
  "name": "users",
  "namespace": "ksql",
  "type": "record"
}
"""
```

### 6. Databricks 上で Wire Header を定義

```python
# Confluent Wire Header の定義
magic_byte_num = 0
wire_header = bytes([magic_byte_num])
wire_header += schema_id_num.to_bytes(4, byteorder='big', signed=False)
print(wire_header)
```

### 7. Databricks から Confluent に書き込む

```python
checkpoint_dir = checkpoint_volume_dir + "/" + topic_name_with_schema

from pyspark.sql.functions import col, struct, lit, concat
from pyspark.sql.avro.functions import to_avro

src_df = spark.readStream.table(src_table_name)

# key 列
src_df = src_df.withColumn("key", col("userid"))

# Value 列（Avro形式へシリアライズ）
src_df = src_df.withColumn("value", struct("registertime", "userid", "regionid", "gender"))
src_df = src_df.withColumn("value", to_avro(col("value"), schema))

# 先頭に Wire Header (magic-byte と schema-id) を付与
src_df = src_df.withColumn("add_bytes", lit(wire_header))
src_df = src_df.withColumn("value", concat(col("add_bytes"), col("value")))

# key と value のみを選択
src_df = src_df.select("key", "value")

(
    src_df.writeStream.format("kafka")
    .option("kafka.bootstrap.servers", bootstrap_servers)
    .option("kafka.security.protocol", "SASL_SSL")
    .option(
        "kafka.sasl.jaas.config",
        f'kafkashaded.org.apache.kafka.common.security.plain.PlainLoginModule required username="{sasl_username}" password="{sasl_password}";',
    )
    .option("kafka.sasl.mechanism", "PLAIN")
    .option("topic", topic_name_with_schema)
    .option("checkpointLocation", checkpoint_dir)
    .trigger(availableNow=True)
    .start()
)
```

### 8. Confluent 上で想定通りデータが書き込まれたことを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/73f80944-669c-3c62-7fa3-2391838fe49d.png)


## まとめ

Databricks からシリアライズしたデータを Confluent に書き込んだ際に、Confluent コンソール上で Value が文字化けしてしまうのは、Confluent の Wire Header（magic-byte や schema-id）を付与していないためです。Databricks 側でヘッダーを正しく付与することで、Confluent コンソールでも文字化けをせずに表示できるようになります。
ただし、運用環境では Confluent Schema Registry を利用するのが一般的であり、今回の方法はワイヤーフォーマットの理解を目的としたものです。本番環境ではぜひ Schema Registry を活用し、スキーマ管理を効率化することをおすすめします。