在我的公司,我们在 Kafka 工作流程中使用 Avro。在这个工作流程中,我们最终使用二进制序列化/反序列化来提醒我们的 API 对象是成功的还是失败的。我们发现,在特定数据条件下,Avro 序列化对象能够反序列化为错误的对象类型。在我们的场景中,我们有一个序列化的成功对象,它能够反序列化为失败的对象类型。这会导致我们的 API 出现问题,因为反序列化的类型不正确。
我对使用 avro 比较陌生,在阅读here 的文档时,这似乎是他们的二进制序列化操作方式的结果。
我编写了一个能够重现此问题的示例 dotnet 6 控制台应用程序。
套餐:
Avro 模式
{
"type": "record",
"namespace": "BinarySerializationTests.Schema",
"name": "FailedObject",
"fields": [
{
"name": "Id",
"doc": "Identifier",
"type": "string"
},
{
"name": "ErrorCode",
"doc": "The failure's error code",
"type": "string"
},
{
"name": "Errors",
"doc": "Collection of all errors",
"type": {
"type": "array",
"items": "string"
}
}
]
}
{
"type": "record",
"namespace": "BinarySerializationTests.Schema",
"name": "SuccessObject",
"fields": [
{
"name": "Id",
"type": "string"
},
{
"name": "RequiredProperty1",
"type": "string"
},
{
"name": "RequiredProperty2",
"type": "string"
},
{
"name": "FirstName",
"type": "string"
},
{
"name": "LastName",
"type": "string"
},
{
"name": "Suffix",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty1",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty2",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty3",
"type": [
"null",
{
"type": "int",
"logicalType": "date"
}
]
},
{
"name": "Email",
"type": "string"
},
{
"name": "CellPhone",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty4",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty5",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty6",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty7",
"type": [ "null", "string" ]
},
{
"name": "Address1",
"type": [ "null", "string" ]
},
{
"name": "Address2",
"type": [ "null", "string" ]
},
{
"name": "City",
"type": [ "null", "string" ]
},
{
"name": "State",
"type": [ "null", "string" ]
},
{
"name": "Zip",
"type": [ "null", "string" ]
},
{
"name": "OfficePhone",
"type": [ "null", "string" ]
},
{
"name": "ExtraProperty8",
"type": [ "null", "string" ]
},
{
"name": "TimeZone",
"type": "string"
},
{
"name": "ExtraProperty9",
"type": [ "null", "string" ]
}
]
}
C# 类是使用 avrogen 工具 生成的。如果需要,我可以提供,它们只是从上面的模式生成的。
测试控制台代码:
using Avro.IO;
using Avro.Specific;
using BinarySerializationTests.Schema;
static byte[] Serialize<T>(T thisObj) where T : ISpecificRecord
{
using (MemoryStream memoryStream = new MemoryStream())
{
BinaryEncoder binaryEncoder = new BinaryEncoder((Stream)memoryStream);
new SpecificDefaultWriter(thisObj.Schema).Write<T>(thisObj, (Encoder)binaryEncoder);
return memoryStream.ToArray();
}
}
static T Deserialize<T>(byte[] bytes) where T : ISpecificRecord, new()
{
using (MemoryStream memoryStream = new MemoryStream(bytes))
{
BinaryDecoder binaryDecoder = new BinaryDecoder((Stream)memoryStream);
T thisObj = new T();
new SpecificDefaultReader(thisObj.Schema, thisObj.Schema).Read<T>(thisObj, (Decoder)binaryDecoder);
return thisObj;
}
}
static bool TryDeserialize<T>(byte[] bytes, out T obj) where T : ISpecificRecord, new()
{
obj = default(T);
try
{
obj = Deserialize<T>(bytes);
return true;
}
catch (Exception ex)
{
return false;
}
}
var success = new SuccessObject
{
Id = Guid.NewGuid().ToString(),
RequiredProperty1 = "RD",
RequiredProperty2 = "400000",
FirstName = "FirstName",
LastName = "LastN",
Suffix = "ABC",
ExtraProperty1 = null,
ExtraProperty2 = null,
Email = "[email protected]",
CellPhone = "",
ExtraProperty4 = null,
ExtraProperty5 = null,
ExtraProperty6 = "",
ExtraProperty7 = "",
Address1 = "1 Wilson Road",
Address2 = "",
City = "Annapolis",
State = "MD",
Zip = "21402-0001",
OfficePhone = "",
ExtraProperty8 = "random",
TimeZone = "Eastern Standard Time",
ExtraProperty9 = null
};
var serializedSuccess = Serialize(success);
if (TryDeserialize(serializedSuccess, out FailedObject failedObject))
{
Console.WriteLine("Deserialized as FailedObject");
}
if (TryDeserialize(serializedSuccess, out SuccessObject successObject))
{
Console.WriteLine("Deserialized SuccessObject");
}
我发现上面的“SuccessObject”类型的数据对于什么会导致它被反序列化为“FailedObject”类型非常挑剔。
例如,如果我将属性“后缀”从“ABC”更改为“A”,它将不再反序列化为“FailedObject”类型。
或者,如果我将属性“RequiredProperty2”从“400000”更改为“500000”,它将不再反序列化为“FailedObject”类型。
当它确实将“SuccessObject”反序列化为“FailedObject”类型时,反序列化是根据我认为与 Avro 文档匹配的对象中的顺序将值放入属性中。
例如:
有人知道为什么会出现这个问题吗?或者如果有人有任何潜在的解决方法?
我能想到的唯一解决方案是将类型组合成一个“响应”类型,其中包含一个“成功”布尔值来确定成功或失败。
除非有人有更好的答案,否则我们使用以下解决方案。
由于 Avro 二进制编码 不包含属性名称信息,只要类型匹配,它只是按顺序分配值。在我们上面的示例中,这意味着我们拥有的列表能够在列表中填充属性,直到它到达第一个不是字符串的属性(ExtraProperty3)。
所以更好地理解这一点,我们的解决方案不是将两个可能的模式作为结果,而是将它们组合成一个模式,该模式将告诉我们它是否成功/失败以及任何相关数据。这样我们只会将二进制数据反序列化为一种类型。