事件只加了一个字段,三个消费者却同时挂了
物流平台把“包裹已签收”事件从 v3 升到 v4,只新增了一个字段:proof_image_url。生产者单测通过,JSON Schema 也通过,灰度后却有三个下游同时报错:客服画像不再更新,结算任务积压,消息重试队列快速增长。
根因并不神秘:三个消费者都用了“禁止额外字段”的严格反序列化。对生产者来说这是兼容新增;对真实消费者来说,它就是无法解析的新消息。

兼容性不是生产者单方面宣布的
Schema 往往描述“事件允许长什么样”,但消费者只依赖其中一小部分字段,并且有自己的解析、枚举和默认值策略。真正的契约应该回答:消费者当前到底使用什么、能容忍什么、不能改变什么。
更容易被忽略的是语义变化。字段仍叫 delivered_at,类型仍是字符串,但从“当地时间”改成 UTC;结构完全合法,结算日却可能跨天。还有枚举从 DELIVERED 改成 SIGNED,旧消费者可能把它当未知状态丢弃。

契约测试必须调用真实消费代码
下面用 Pydantic 表示一个消费者边界。额外字段选择 ignore,是消费者明确做出的兼容策略;关键字段仍然严格校验:
from datetime import datetime, timezone
from pydantic import BaseModel, ConfigDict, StrictStr, field_validator
from typing import Literal
class DeliveredEvent(BaseModel):
model_config = ConfigDict(extra="ignore")
event_id: StrictStr
shipment_id: StrictStr
status: Literal["DELIVERED"]
delivered_at: datetime
@field_validator("delivered_at")
@classmethod
def require_timezone(cls, value):
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("delivered_at 必须带时区")
return value.astimezone(timezone.utc)
def consume(raw: dict) -> tuple[str, str]:
event = DeliveredEvent.model_validate(raw)
return event.shipment_id, event.delivered_at.date().isoformat()
def test_v4_additive_field_is_compatible():
raw = {"event_id":"e-1", "shipment_id":"s-9",
"status":"DELIVERED", "delivered_at":"2026-09-05T10:00:00+08:00",
"proof_image_url":"oss://proof/9.jpg"}
assert consume(raw) == ("s-9", "2026-09-05")
不要为了让契约文件好看,绕过真实 consume 函数,只拿通用 JSON 客户端做验证。那样只能证明示例合法,不能证明线上消费者真的能处理。
结构通过后,还要验证消息行为
事件系统至少还要覆盖四种时序:同一 event_id 重复投递;v3 与 v4 在灰度期乱序到达;消费者处理成功但 ACK 丢失;历史消息在修复后重放。
消费端应把幂等边界落到数据库,而不是放在进程内集合:
CREATE TABLE consumer_inbox (
consumer_name TEXT NOT NULL,
event_id TEXT NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (consumer_name, event_id)
);
-- 插入成功才执行业务;冲突表示该消费者已处理过
INSERT INTO consumer_inbox(consumer_name, event_id)
VALUES ('settlement-v2', :event_id)
ON CONFLICT DO NOTHING;

上线前输出一张兼容矩阵
生产者 v4 不只要验证最新消费者。灰度期间仍在线的画像 v2、结算 v3、客服 v1 都应进入矩阵:能否解析、语义是否一致、重复是否安全、回滚后是否还能消费新事件。
消费者驱动契约的价值就在这里:每个消费者只声明自己真实依赖的最小交互,生产者流水线逐一回放。新增假设时先验证生产者,生产者修改时反向验证所有消费者。
“只是加一个字段”从来不是风险结论,它只是变更描述。能不能安全发布,要由正在运行的消费者回答。
- 点赞
- 收藏
- 关注作者
评论(0)