- 后端
- 微服务
【免费下载链接】akka.net
Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
导读
Akka.NET 1.6 以SerializerV2作为规范化的序列化抽象(基于IBufferWriter<byte>/ReadOnlySequence<byte>,与 1.6 的源生成序列化器、AOT 友好、低分配远程通信目标一致)。为了让既有 legacy 序列化器平滑过渡到 V2,仓库在 V2Port 测试工具包 中定义了一套标准做法:每个 legacy 序列化器获得一个id + 40、共享同一组 manifest的 V2 孪生序列化器,并用V2PortSpec基类 + Golden Snapshot 逐字节钉住线上格式。读完本文,你将掌握如何为一个 legacy 序列化器编写 V2 port 测试、如何利用MixedBindingPair验证滚动升级中的双向互操作,以及如何按仓库规定的三步流程捕获和接受 golden snapshots,确保已发布的线上格式永不漂移。
一、背景:为什么需要 "V2 port" 与 id + 40 约定
1.1 SerializerV2 与缓冲优先的序列化 API
仓库中的 SerializerV2 是 V2 序列化器的基类,它仍然继承自SerializerWithStringManifest(保证 1.5 契约下持久化插件等调用方可以读到 manifest),但核心方法变为:
int Serialize(object obj, IBufferWriter<byte> writer)—— 写入调用方持有的缓冲区,返回实际写入的字节数;object Deserialize(ReadOnlySequence<byte> bytes, string manifest)—— 从序列化输入直接读取;int SizeHint(object obj)—— 返回精确的序列化字节数,无法廉价预测时返回UnknownSize = -1;byte[] ToBinary(...)/FromBinary(...)—— 为兼容边界保留的 byte[] 桥接方法,内部按SizeHint预分配ArrayBufferWriter<byte>。
这一转变的背景记录在 serializer-v2 提案 中:旧的byte[]序列化 API 在每次序列化/反序列化路径上都强制分配,与 1.6 的源生成序列化器、AOT 友好、低分配远程通信目标不兼容;同时经典 Akka.Remote、Akka.Persistence、DistributedData、delivery 等模块都会受影响,因此该变更被定位为"原子化的 V2 基础"。
1.2 "Port" 的含义:legacy 序列化器的 V2 孪生
所谓port(移植),指的不是从零重写序列化器,而是:一个 legacy 序列化器获得一个V2 孪生,两者共享完全相同的 manifest 字符串,只是序列化器 id 不同(V2 的 id = legacy 的 id + 40,且落在 40–79 保留块内),并且 V2 行是只读的(read-only,即没有绑定)——消息类型仍然绑定到 legacy 序列化器。
为什么 id 加 40?Akka.NET 的序列化器 id 是线上格式的一部分:两节点对 id 理解不一致就无法互读。官方文档 序列化 ID 码表 与 序列化指南 都明确 0/1–40 保留给 Akka.NET 内部序列化器使用;40–79 这段则是为 V2 port 预留的孪生块。通过legacyId + 40的确定性映射,节点在读到 V2 id 时可以直接推断出对应的 legacy id 语义。
1.3 模块表与 SerializationSetup:V2 序列化器的两种注册方式
README 强调:两个序列化器都必须按"已部署节点实际拥有它们"的方式注册——即要么出现在某个模块的表(module's table)中,V2 行只读(无 bindings);要么通过SerializationSetup注册。仓库中模块序列化器表(ModuleSerializers)是唯一事实来源,其内容由 SerializerTableSpec 像公共 API 一样审批(approve),任何表内容变更都会以 diff 形式出现在代码评审中。
二、V2Port 工具包的组成
工具包位于 src/core/Akka.Tests.Shared.Internals.Xunit3/V2Port/,共三个源文件:
| 文件 | 职责 |
|---|---|
| V2PortSpec.cs | 抽象测试基类:定义三个抽象成员,并内置 8 个测试(含两个 golden 测试) |
| MixedBindingPair.cs | 滚动升级互操作工具:同时启动"V2 节点"与"legacy 节点",双向交换字节 |
| HexDumpFormatter.cs | 把线上字节渲染成可评审的带注释十六进制转储(供 Verify 快照使用) |
三、编写一个 Port Spec:三步走
按 README 的要求,为某个 legacy 序列化器编写移植测试分三步:
3.1 派生 V2PortSpec 并实现三个抽象成员
从V2PortSpec派生,需要实现:
CreateLegacy(ExtendedActorSystem system)—— 构建 legacy 序列化器;CreateV2(ExtendedActorSystem system)—— 构建 V2 序列化器。注意基类注释:它会被调用很多次,必须每次返回全新实例;Cases—— 语料库:IReadOnlyList<V2PortCase>,每个 manifest 一个V2PortCase,且值必须是固定的。
V2PortCase是工具包定义的三元组 record(见 V2PortSpec.cs):
public sealed record V2PortCase(object Message, string Manifest, string Name);Message:确定性消息——不允许Guid.NewGuid()、不允许DateTime.Now,否则 golden 字节会漂移;Manifest:legacy 序列化器给该消息的 manifest 令牌,V2 序列化器必须复用同一令牌;Name:在语料库中唯一,用于命名快照文件。
把output(以及可选的Config或ActorSystemSetup)传给基类构造函数即可,sourceFile参数由编译器通过[CallerFilePath]自动填充,无需手工传入。基类提供了两个构造函数重载,分别对应"纯配置"与"通过SerializationSetup注册序列化器"两种系统构建方式。
3.2 放入 DynamicTypeLoading 专属集合
必须把 spec 放在[Collection(DynamicTypeLoadingCollection.Name)]集合中。原因是基类中有一个检查会翻转进程级开关Akka.DynamicTypeLoading(AppContextswitch),该开关影响动态类型加载行为,进程内不能与其他测试并发。仓库中每个需要它的测试项目都自带一个[CollectionDefinition(Name, DisableParallelization = true)]定义,例如 ShardingSerializersSpec.cs 中的注释明确说明:"AppContext 开关是进程级的,翻转Akka.DynamicTypeLoading的 spec 绝不能与其他测试并行运行"。
3.3 滚动升级互操作:MixedBindingPair
对于需要验证跨版本互操作的场景,使用MixedBindingPair:
await using var pair = MixedBindingPair.Create(name, v2Config, legacyConfig); pair.AssertRoundTripsBothWays(cases, v2Id, legacyId);MixedBindingPair.Create(name, v2Config, legacyConfig)会启动两个进程内节点:name-v2与name-legacy,各自的配置都以AkkaSpec.AkkaSpecConfig作为 fallback,并必须注册该 port 的序列化器;MixedBindingPair.BindTo(alias, params Type[] types)生成把指定类型绑定到某序列化器别名的 HOCON(akka.actor.serialization-bindings),用于构造两侧配置;AssertRoundTripsBothWays(cases, v2Id, legacyId)把每个 case双向发送:V2 节点 → legacy 节点、legacy 节点 → V2 节点,按"序列化器 id + manifest"的方式模拟远程节点交换字节(见 MixedBindingPair.cs),并断言写入方确实使用预期 id、manifest 与 case 一致、读取方还原出原消息。
仓库自带的可运行示例是 MixedBindingPairSpec.cs:PairLegacySerializer(id 39,manifest"P",UTF-8 文本)与PairV2Serializer(id 79,manifest"P",在文本前加 0x01 前缀字节),并通过BindTo生成绑定配置。它还包含一个反例测试:当 V2 节点仍被钉在 legacy 绑定时,AssertRoundTripsBothWays会抛出异常,证明该检查确实能抓住"升级未生效"的配置错误。
四、基类自动执行的检查
V2PortSpec内置 8 个[Fact],派生 spec 无需编写任何断言逻辑,继承即可获得:
| DisplayName(测试名) | 检查内容 |
|---|---|
Should_UseReservedIdBlock_When_PortIsDeclared | V2 id = legacy id + 40,且落在 40–79 内 |
Should_ReuseLegacyManifests_When_V2SerializesCorpus | legacy 与 V2 对语料库每个消息返回的 manifest 都与V2PortCase.Manifest一致 |
Should_RoundTripEveryManifest_When_V2Serializes | V2 的FromBinary、Deserialize(ReadOnlySequence)往返还原;Serialize报告的字节数与ToBinary输出一致;SizeHint精确(若非UnknownSize) |
Should_RoundTripEveryManifest_When_LegacySerializes | legacy 序列化器自身往返还原每个 case |
Should_DecodeBothIds_When_NodeHoldsBothSerializers | 节点同时持有两个序列化器时,legacy id 与 V2 id 各自都能解码 |
Should_KeepLegacyBinding_When_V2RowIsReadOnly | V2 行只读:FindSerializerFor/FindSerializerForType仍解析到 legacy 序列化器 |
Should_MatchGoldenV2Bytes_When_V2Serializes | V2 golden 快照(Snapshots/<前缀>.<case>.v2.verified.txt) |
Should_MatchGoldenLegacyBytes_When_LegacySerializes | legacy golden 快照(Snapshots/<前缀>.<case>.legacy.verified.txt) |
其中Should_ResolveV2Id_When_DynamicTypeLoadingIsOff会临时把Akka.DynamicTypeLoading开关置为false,验证关闭动态类型加载时 V2 id 依然能解析——这正是 AOT / 严格部署场景下的关键保证,也是 spec 必须独占进程的原因。断言采用Equals优先、否则按运行时类型做结构等价(RespectingRuntimeTypes)的AssertSame策略(见 V2PortSpec.cs)。
五、Golden Snapshots:把线上格式钉死在文本 diff 里
5.1 为什么是"文本快照"而不是 .bin
Golden 测试对每个 case 用HexDumpFormatter生成带注释的十六进制转储(不是原始.bin文件),再用 Verify 与已提交的.verified.txt比对。这样任何线上格式变更都会以可评审的文本 diff出现在 PR 中,而不是无法阅读的二进制 diff。快照文件落在派生 spec 源文件旁的Snapshots/目录下,命名规则为<Spec>.<case>.legacy.verified.txt与<Spec>.<case>.v2.verified.txt,<Spec>默认取 spec 的类型名,可通过重写SnapshotPrefix改变。
HexDumpFormatter的输出格式(见 HexDumpFormatter.cs)为:头部块(case、message-type、manifest、serializer-id、byte-count)+ 经典offset | hex | ascii转储(每行 16 字节,8 字节中点分栏)。头部块正是让 diff 可评审的关键:字段重排会表现为"头部不变下的字节级 diff",而 manifest 变更会直接显示在头部行。仓库中 Akka.Serialization.V2.Tests/WireSnapshots 使用同一格式,例如collection-array-populated.verified.txt的实际内容:
case: collection-array-populated message-type: IntArrayMessage manifest: coll-int-array-v1 serializer-id: 120105 byte-count: 6 0000 81 01 93 0a 14 1e |......|5.2 捕获与接受的三步流程
首次运行或格式变更时,测试会失败并在.verified.txt旁写出<name>.received.txt(该文件已被 .gitignore 忽略,属于临时 diff 产物,永不提交)。阅读 diff 确认无误后,把 received 覆盖到 verified 即可,与WireSnapshots/README.md的接受步骤一致:
for f in <spec folder>/Snapshots/*.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; doneREADME 规定捕获顺序必须是:
- 先捕获 legacy 快照,再动 legacy 序列化器,并提交它们——它们证明"今天的字节仍然能被解码":
dotnet test src/core/<TestProject> -c Release --filter "DisplayName=Should_MatchGoldenLegacyBytes_When_LegacySerializes" - V2 序列化器工作正常后捕获 V2 快照——提交前务必通读:已发布的格式就是线上格式:
dotnet test src/core/<TestProject> -c Release --filter "DisplayName=Should_MatchGoldenV2Bytes_When_V2Serializes" - 不接受任何文件地完整运行一遍,两个测试必须全部通过。
其中<TestProject>是派生 spec 所在测试项目的占位符;--filter精确匹配基类测试的DisplayName。VerifyAll的实现保证"每个 case 都会先跑一遍再失败",因此首次运行会一次性写出全部.received.txt,方便统一接受。
六、不可违背的红线规则
README 末尾明确列出三条纪律,也是V2Port机制能保护线上兼容性的根基:
- 永远不要手工编辑
.verified.txt来让测试通过——快照是"线上格式"的证据,手工篡改等于掩盖回归; - 如果 legacy 快照在
git diff中发生变化,立即停下:legacy 格式绝不能改变,否则已部署的旧节点将无法互读; - 已发布的 V2 格式只能增长:只能新增字段,绝不能修改或复用已有字段;CI 只做比较,缺失或过期的快照就是测试失败,而不是被静默重新生成。
这套规则与仓库中其他"线上契约审批"机制一脉相承:模块序列化器表(别名、id、绑定)由 SerializerTableSpec 以公共 API 同等方式审批;WireSnapshots语料库则为生成器支持的每种结构形态各留一份字节快照,作为最终 protobuf 到 MessagePack 迁移的字节 golden 基线。
七、小结
V2Port 测试工具包把"legacy 序列化器向 V2 移植"这件高风险工作变成了可重复、可评审的流程:V2PortSpec基类内置的 8 项检查覆盖 id 保留块、manifest 复用、双向解码、只读绑定与动态类型加载关闭等全部关键不变量;MixedBindingPair用两个进程内节点模拟滚动升级中途"一节点跑 V2、一节点钉 legacy"的真实拓扑;golden snapshots 则用文本 diff 把线上格式钉死在每次提交里。对任何正在把 Akka.NET 1.5 序列化器升级到 1.6 V2 体系的开发者,这套工具与"先捕获 legacy、后捕获 V2、全程不手工改快照"的流程,就是保证线上字节兼容的最短安全路径。
- 后端
- 微服务
【免费下载链接】akka.net
Canonical actor model implementation for .NET with local + distributed actors in C# and F#.
相关推荐
Jest 内部测试工具包 @jest/test-utils 实战指南:alignedAnsiStyleSerializer 快照序列化器与条件测试工具
Jest 内部测试工具包 @jest/test utils 实战指南:alignedAnsiStyleSerializer 快照序列化器与条件测试工具 @jes
测试质量保障代码覆盖率开发工具Akka.NET 持久化测试实战指南:使用 Persistence TestKit 模拟 Journal 与 Snapshot Store 故障场景
Akka.NET 持久化测试实战指南:使用 Persistence TestKit 模拟 Journal 与 Snapshot Store 故障场景 持久化是
后端微服务Akka.NET 序列化完全指南:消息编码、自定义序列化器、Hyperion 迁移与安全加固
Akka.NET 序列化完全指南:消息编码、自定义序列化器、Hyperion 迁移与安全加固 序列化是 Akka.NET 消息传递与网络透明的基石:本地与分布式
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考