Customizing Serializers for Flink State

Liao Jiayi Liao Jiayi #Flink#Apache Flink

Custom state serializers can improve compatibility and performance in complex Flink applications.

Translated from Chinese with AI · Read the original

Customizing Flink state serialization is an advanced technique that can improve compatibility and performance in complex scenarios.

What a State Serializer Does

These two lines create an Integer ValueState:

// 创建 Integer 类型的 ValueState
ValueStateDescriptor<Integer> valueState = new ValueStateDescriptor<>("value", Types.INT);
ValueState<Integer> state = getRuntimeContext().getState(valueState);

Passing Types.INT tells Flink that the state holds Integers, for which it creates an IntSerializer. That serializer has two roles:

  • During snapshots, serialize Integer state values into binary data for persistent storage.
  • During restore, deserialize stored binary data back into Integer objects.

A state serializer therefore defines the state’s binary format. Customizing it can solve common problems such as:

  1. High overhead for complex objects, such as Kryo writing redundant className information.
  2. Failed recovery after fields are added to or removed from state objects.

Basic Concepts

A complete state serializer consists of a Serializer and SerializerSnapshot:

  • Serializer defines serialization and deserialization for a type.
  • SerializerSnapshot defines how serializer metadata is serialized as part of the snapshot.
  • TypeSerializerSchemaCompatibility describes compatibility between the user-defined TypeSerializer after restart and the one in the snapshot.

The official Custom Serialization for Managed State documentation explains these concepts in more detail.

Implementing a Serializer

We will examine DecimalDataSerializer, which serializes DecimalData.

First, the DecimalData structure:

public final class DecimalData implements Comparable<DecimalData> {
final int precision; // 精度(字段长度)
final int scale; // 范围(小数的位数)
final long longVal; // 精度较小时,选择用 long 值表示
BigDecimal decimalVal; // 精度较大时,选择用 java.math.BigDecimal 表示
// 判断是否超出最大精度(即是否可以使用 long 值表示)
public boolean isCompact() {
return precision <= MAX_COMPACT_PRECISION;
}
// 转换成 long 值
public long toUnscaledLong() {
if (isCompact()) {
return longVal;
} else {
return toBigDecimal().unscaledValue().longValueExact();
}
}
// 转换成 bytes
public byte[] toUnscaledBytes() {
return toBigDecimal().unscaledValue().toByteArray();
}
}

Its corresponding serializer:

public final class DecimalDataSerializer extends TypeSerializer<DecimalData> {
private final int precision;
private final int scale;
// 构造函数,初始化 precision 和 scale
public DecimalDataSerializer(int precision, int scale) {
this.precision = precision;
this.scale = scale;
}
@Override
public void serialize(DecimalData record, DataOutputView target) throws IOException {
if (DecimalData.isCompact(precision)) {
// 当前精度小,使用 long 值表示,写出 long 值
assert record.isCompact();
target.writeLong(record.toUnscaledLong());
} else {
// 当前精度大,使用 BigDecimal 表示
byte[] bytes = record.toUnscaledBytes();
target.writeInt(bytes.length);
target.write(bytes);
}
}
@Override
public DecimalData deserialize(DataInputView source) throws IOException {
if (DecimalData.isCompact(precision)) {
// 当前精度小,读取 long 值,初始化 DecimalData
long longVal = source.readLong();
return DecimalData.fromUnscaledLong(longVal, precision, scale);
} else {
// 当前精度大,读取 bytes,初始化 DecimalData
int length = source.readInt();
byte[] bytes = new byte[length];
source.readFully(bytes);
return DecimalData.fromUnscaledBytes(bytes, precision, scale);
}
}
}

The code shows two core methods:

  • serialize: Converts DecimalData into binary data written to DataOutputView.
  • deserialize: Reads binary data sequentially from DataInputView and reconstructs DecimalData.

You do not need to decide when these methods run. Whether used for RPC or a local/remote filesystem, the corresponding OutputStream and InputStream are wrapped in DataOutputView and DataInputView.

Supporting Multiple Versions

If precision or scale changes after a job modification, DecimalData.isCompact(precision) may produce a different result, making existing bytes impossible to deserialize. What must change to build a serializer that tolerates such modifications?

Include Version Information

To allow precision and scale to change, modify the serialize and deserialize methods as follows:

int currentVersion = 2;
@Override
public void serialize(DecimalData record, DataOutputView target) throws IOException {
target.writeInt(currentVersion); // 新增版本信息
// ... 和之前一致
}
@Override
public DecimalData deserialize(DataInputView source) throws IOException {
int version = source.readInt();
if (version == currentVersion) {
return deserializeCurrentVersion(source);
} else {
return deserializeVersion1(source);
}
}
// 解析当前版本的数据结构
private DecimalData deserializeCurrentVersion(DataInputView source) {
// 使用当前版本的 precision 和 scale 进行解析
}
// 解析 Version 1 版本的数据结构
private DecimalData deserializeVersion1(DataInputView source) {
// 使用 Version 1 中的 precision 和 scale 进行解析并转化成当前结构的数据
}

Adjust Compatibility Checks

After precision or scale changes, the snapshot’s DecimalDataSerializer no longer matches the newly created one. Flink checks these properties and throws TypeSerializerSchemaCompatibility.incompatible() on a mismatch. With version information included, we can instead return TypeSerializerSchemaCompatibility.compatibleAsIs().

There are four compatibility outcomes:

  • COMPATIBLE_AS_IS: Compatible; use the new user-defined serializer going forward.
  • COMPATIBLE_AFTER_MIGRATION: Compatible after rewriting state: deserialize with the snapshot’s serializer, then serialize with the new one.
  • COMPATIBLE_WITH_RECONFIGURED_SERIALIZER: Compatible; return a reconfigured serializer for future use.
  • INCOMPATIBLE: Incompatible; the job throws an exception and exits.

References

  1. DecimalDataSerializer.java
  2. Custom Serialization for Managed State

Custom serializers are common in platform applications, especially DataStream-based frameworks. Users store business state whose fields frequently change. Flink’s built-in implementations, such as IntSerializer and DecimalDataSerializer, provide useful examples.