为速度而设计:无模式事件摄取的理由
SchemaBridge Team · 2025-12-08 · Event Ingestion, JSON, DX
事件驱动系统中的速度与严格类型之争。我们为何选择原始 JSON,而非严格的类型定义。
模式悖论:是友是敌?
在传统企业软件中,严格的模式(SQL、Protobuf、WSDL)是可靠性的基石。契约很简单:我定义数据的形状,你遵守它,编译器保证我们不会崩溃。在我们同时掌控管道两端的受控环境中,这种做法已经服务了我们几十年。它提供编译期安全、高效的二进制序列化,以及开发者眼中清晰的"唯一事实来源"。
但在现代的高速集成环境中——SaaS 厂商每周都在修改载荷,遗留 ERP 输出"灵活"的 CSV,内部微服务每月都有新生和退役——刚性模式就成了一件束身衣。它以惯性为代价换来安全。当周围的世界是流动的,刚性契约不是地基,而是故障点。模式悖论就在于:你越想用严格类型保护系统,外部世界一变化,系统就越脆弱。
历史演进:从 COBOL 到 JSON
要理解对无模式系统的需求,我们必须回顾数据交换的演进。在大型机计算的早期,数据存储在定长记录(COBOL COPYBOOK)中。想增加一个字段,就必须重新编译所有读取该记录的程序。这是终极的刚性模式。那是"单机思维"的时代,存储成本高到每个字节都必须在固定位置上精打细算。没有层级的空间,没有可选字段的空间,更没有演进的空间。记录中的每个字符都是宝贵资源,任何改动都是一场需要数周规划和测试的地震。
在 1980 和 90 年代,我们转向了关系型数据库和 SQL。这是巨大的进步,因为它引入了结构化关系的概念。但它也带来了数据库迁移。加一列意味着停机、锁表,以及 DBA 与开发者之间的周密协调。每当开发者想要创新,模式仍是一堵必须翻越的墙。即使 Hibernate 等 ORM 兴起,表结构底层的刚性依然是可行与否的最终裁决者。
随后,90 年代末出现了 XML 和 SOAP。它们引入了"标签"的概念,带来了一定的灵活性。你可以添加一个 XML 标签而不一定会破坏解析器。然而,业界很快又加上了 XSD(XML Schema Definition)和 WSDL,把刚性带了回来。我们花了多年时间与命名空间搏斗,与那些只要错一个字符就拒收消息的复杂企业服务总线较劲。那是"XML 的黑暗时代",模式的开销比实际数据还要占带宽。
如今我们有了 JSON 和 REST。JSON 天生灵活,它只是一个键值映射。然而,我们的工程本能仍驱使我们用严格类型(TypeScript 接口、Java DTO、Avro 模式)把这份灵活性包裹起来。我们在试图把 1970 年代大型机的刚性强加给 2020 年代的云事件。为什么?因为我们害怕未知。我们害怕一个缺失的字段会让服务崩溃。但正如下文所示,人们正在用错误的工具应对这种恐惧。
财务分析:集成维护的"隐性税"
让我们量化模式刚性的成本。在典型的中大型工程组织中,集成维护是一场"静默危机"。它不会作为单独科目出现在资产负债表上,却严重拖累生产力。
维护的算术
只要留心,这种模式很容易看清。一个拥有大量外部 SaaS 集成的组织,会看到厂商定期修改载荷,而每一次破坏严格解析器的变更都会触发同样的循环:发现问题、更新 DTO/模式、测试、通过 CI/CD 部署。每次故障耗费几个小时,乘以一年中的多次故障,累积起来就是对资深工程师时间的大量且持续的消耗。
但真正的成本是机会成本。当资深工程师忙着为 Stripe 的最新变更更新 DTO 时,他们就没有在构建推动业务前进的自动化欺诈检测新功能。久而久之,这笔税会复利般累积成持续的速度损失,可能让公司远远落后于更敏捷的竞争对手。
数据流动性的哲学:schema-on-read
在 SchemaBridge,我们倡导一种根本性的思维转变:schema-on-read。
我们不在数据进入的那一刻进行校验(schema-on-write),而是先摄取事件原始的、层级化的真实内容。JSON 载荷以到达时的原样随运行(run)一起持久化,无需预先声明模式。只有在某个具体业务流程需要这些数据的那一刻,我们才应用模式——更准确地说,应用转换。
数据流动性为何胜出
1. 零接触摄取:你可以在几秒钟内开始接收新提供方的事件。把提供方的 webhook 指向一个 SchemaBridge 入口点(由摄取层提供服务),每个 JSON 事件都会立即启动一次有记录的运行。数据意味着什么,可以以后再弄清楚。
2. 应对未知的保险:如果提供方今天新增了一个你用不上的字段,它仍会被保留在原始 JSON 中。原始载荷会在你所订阅方案的保留期内保存,因此如果几周后你发现确实需要这个字段,原始输入依然保留:你可以通过更新后的映射逐个重放运行(或重新投递其载荷),而不必向提供方索要旧数据。
3. 解耦的演进:摄取层和转换层可以按不同的速度演进。你可以一天多次更新业务逻辑,而无需触碰入口点。
延迟绑定的数学论证
在计算机科学中,延迟绑定(late-binding)是指把标识或类型的解析推迟到执行的那一刻。这正是 Ruby、Python 等动态语言在某些任务上如此强大的原因。
schema-on-read 就是把延迟绑定应用到数据基础设施上。通过推迟映射,你从刚性图(每次变更都需要完全重建)转向灵活路径(路径可以边走边适应地形)。
从数学上看,$N$ 个生产者与 $M$ 个消费者之间可能的映射数量是 $N \times M$。如果每个生产者和消费者都必须就严格模式达成一致,你就会面临巨大的协调问题。如果使用带延迟绑定的无模式桥接,问题就缩减为 $N + M$ 个映射,每个映射都是局部且独立的。这就是实现真正的工程水平扩展的方式。
JSONata:函数式事件处理的大师课
要让 schema-on-read 切实可行,你需要一门为探索而设计的语言。我们选择了 JSONata。JSONata 不只是一门查询语言,它是一个直接作用于原始 JSON 层级的函数式转换引擎。
JSONata 表达式剖析
设想一个来自遗留 ERP 的载荷,返回一个订单列表。每个订单都有复杂的嵌套结构。你想提取所有金额高于 500 美元、且当前处于 'SHIPPING' 状态的订单中的全部零件编号。
传统代码(Javascript):
const parts = payload.orders
.filter(o => o.total > 500 && o.status === 'SHIPPING')
.flatMap(o => o.items)
.map(i => i.partNumber);
这段代码很脆弱。如果 orders 为 null,或某个订单缺少 items,它就会崩溃。
JSONata 的精妙之处:
orders[total > 500][status = 'SHIPPING'].items.partNumber
这个表达式具有路径容错性。没有 items 的订单只是不贡献任何结果;完全没有 orders 的载荷会求值为空,而不是引发空指针崩溃。它是在"探索"数据,而不是"断言"数据。
坦白说,有两点需要注意。第一,JSONata 在类型上并不会变魔术:如果某个厂商开始把 "total": "600" 作为字符串发送,比较会直接报错,而不是去猜测。第二,在 SchemaBridge 上,完全不产生输出的映射会使该步骤失败,而不是把空值传递到下游。这是有意为之:映射突然什么都匹配不到,几乎总是上游数据形状发生变化的信号,而你希望这种情况能够被明显地暴露出来。
高级模式:深层后代选择
JSONata 最强大的特性之一是后代运算符 **。无论键位于层级的哪个位置,它都能找到。
**.tracking_number
如果下游各提供方对运单号的嵌套方式各不相同,这一个表达式就能在每个载荷版本中把它们全部找出来。这就是结构韧性的定义。它把对某个特定键的脆弱搜寻,变成了对真相的灵活检索。
高级模式:即时数据重组
JSONata 允许你在一次遍历中重建整个 JSON 对象。
orders.{ "order_id": ID, "summary": $join(items.name, ', ') }
在传统代码中,这需要对象映射、字符串拼接和数组迭代。在 JSONata 中,它就是对期望状态的声明式投影。当你需要把一个复杂事件的简化摘要发送到 Slack 通知或移动应用时,这一点尤其有用。
无表数据的运维:安全与校验
批评者常常问:"如果不用模式,我们如何防止垃圾数据进入系统?"
答案是:无模式 != 未校验。我们只是把校验移到了 vertex 层级。
- 入口点:接受任何 JSON 请求体,无需预先声明模式。它们是高吞吐量的接收口,而不是放置业务规则的地方。
- 检查步骤:在图的前部放置一个检查载荷的转换步骤,例如
$exists(device_id) and $type(reading) = 'number',并通过条件边根据结果进行路由。如果你更希望遇到坏数据就停下,可以让检查使该步骤失败:运行会显式失败,修复数据或映射后,你可以从失败的步骤开始重试。对于需要人工介入的情况,Approval 步骤会暂停运行以供人工审核。 - 显式的映射失败:一个突然什么都不产出的 JSONata 映射会使其步骤失败,而不是把空数据传到下游,因此该运行会在监控中显示为红色。你能在出错的那个步骤就发现模式变化,而不是在三个系统之后。
案例研究:横跨 50 个区域的数据摄取网格
设想一个全球 IoT 设备群,从许多区域摄取遥测数据,每个区域运行的传感器固件版本都略有不同。每个区域都有自己的 JSON"方言"。
挑战
假设团队一开始使用的是基于 SQL 的传统摄取系统和严格的表模式。每当某个区域推出固件更新,该区域的摄取管道就会中断,因为新固件新增了一个数据库中没有对应列的 battery_health_v2 字段。数据团队陷入长期的"救火"模式,在许多生产数据库上执行 ALTER TABLE 命令,并在每个维护窗口中丢失事件。
SchemaBridge 方案
现在设想同一个团队转向无模式策略。
1. 统一采集:每个区域都把数据发送到同一个 SchemaBridge 入口点。摄取层不关心模式;它把每个原始事件随其运行一起持久化。
2. 读时映射:团队为每个固件版本创建一个规范化步骤。条件边根据固件版本字段,把每个原始事件路由到相应的步骤。
3. 无代码升级:新固件版本发布时,没有数据库需要更新。团队复制现有步骤,更新 JSONata 映射以纳入新字段,然后部署即可。
可能的结果
- 停机:固件更新将不再迫使数据团队中断摄取。
- 数据完整性:即使原始传感器数据中包含团队尚未准备好处理的字段,也能被采集下来。
- 工程专注度:摆脱数据库迁移之后,数据团队可以专注于基于所摄取原始数据的预测性维护——而且由于"额外"字段被保留而非丢弃,他们有可能发现诸如新出现的电池缺陷之类的问题,而刚性的 schema-on-write 系统早已把这些数据丢掉了。
结论:速度是一种设计选择
严格模式是一种选择。它选择了静态安全优先于动态速度。在封闭系统中,这是合理的选择。但在互联的分布式生态中,这是一条通向失败的选择。
请按照世界的本来面目设计你的系统——不可预测、不断演进、层级化。为速度而设计。用 SchemaBridge 来设计。拥抱数据的流动性,你就能以一种速度去构建、扩展和创新,而那些困在刚性 DTO 和数据库迁移中的竞争对手只能望尘莫及。
另请参阅:精通扇出——借助 Spawner vertex 从摄取走向执行,它在有界背压下把大批量工作分散到众多条目上。