为什么需要演进
eventd v2 是响应式 HTTP 服务,v3 是流处理管道。演进需求:
- v2 已有业务逻辑,不能停机迁移
- v3 需要消费 v2 产生的事件
- 可能需要保留 v2 的某些接口
演进策略
阶段 1:并行运行(灰度)
v2 事件生产
├── Topic "eventd-raw" → v3 清洗 → v3 面板
└── v2 数据库 → v2 API → v2 面板(保留)
用户:
10% → v3 面板
90% → v2 面板
阶段 2:数据验证
并行期持续 N 天(如 7 天):
→ 每天对比 v2 和 v3 的指标数据
→ 误差 < 1% → 继续
→ 误差 > 1% → 排查修正
阶段 3:完全切换
100% 流量 → v3 面板
v2 面板下线(或保留只读)
v2 API 保留(用于历史数据查询)
事件格式兼容
v2 事件 → v3 事件:新增字段,并把 timestamp 改名为 eventTime。新增字段可以给默认值,改名必须由下游显式兼容,不能算「向后兼容」。
v2 格式:
{
"eventId": "uuid",
"type": "ORDER",
"userId": "alice",
"amount": 100.0,
"timestamp": 1700000000000
}
v3 格式(新增字段 + 一个字段改名):
{
"eventId": "uuid",
"type": "ORDER",
"userId": "alice",
"amount": 100.0,
"eventTime": 1700000000000,
"version": 1,
"source": "eventd-v2",
"symbol": "BTC",
"status": 0,
"ipAddress": null,
"device": null,
"metadata": {}
}
| 变化 | 字段 | 兼容处理 |
|---|---|---|
| 改名 | timestamp → eventTime | 下游必须显式兼容,不能算「向后兼容」 |
| 新增 | version / source | 缺省 1 / eventd-v2 |
| 新增 | symbol / status / ipAddress / device | symbol 缺省 UNKNOWN,status 缺省 0,ipAddress 和 device 缺省 null |
| 新增 | metadata | 缺省空 Map,端到端延迟等观测字段可放这里 |
清洗管道处理兼容性:
public class EventDeserializer {
public Event deserialize(String json) {
// 兼容 v2 格式(缺少 version/source/symbol)
JsonNode node = JsonMapper.parse(json);
return Event.builder()
.eventId(node.get("eventId").asText())
.type(node.get("type").asText())
.userId(node.get("userId").asText())
.amount(node.has("amount") ? node.get("amount").asDouble() : 0)
.eventTime(node.has("eventTime") ? node.get("eventTime").asLong()
: node.get("timestamp").asLong()) // 兼容旧字段名
.version(node.has("version") ? node.get("version").asInt() : 1)
.source(node.has("source") ? node.get("source").asText() : "eventd-v2")
.symbol(node.has("symbol") ? node.get("symbol").asText() : "UNKNOWN")
.status(node.has("status") ? node.get("status").asInt() : 0)
.build();
}
}
双线并行切换
Kafka Topic 配置:
// v3 的 Kafka Source 可以消费多个 Topic
KafkaSource<Event> source = KafkaSource.<Event>builder()
.setBootstrapServers("kafka:9092")
.setTopics("eventd-raw-v2", "eventd-raw-v3") // 两个 Topic
.setGroupId("eventd-analytics")
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST))
// 必须在同一个 schema 里兼容两种格式(timestamp/eventTime 回退),
// 见上一节的 EventDeserializer;7.1 的 schema 只认单一种字段名
.setValueOnlyDeserializer(new V2V3DeserializationSchema())
.build();
或者使用两个独立的 Consumer Group(更灵活):
Group "eventd-cleansing-v2" → Topic "eventd-raw-v2"
Group "eventd-cleansing-v3" → Topic "eventd-raw-v3"
灰度比例控制
eventd v2 发送事件时控制比例:
public class EventPublisher {
public Mono<Void> publish(Event event) {
String json = JsonMapper.toJson(event);
// Spring Kafka 3.x 的 KafkaTemplate.send 返回 CompletableFuture,
// 没有 then();要先转成 Mono 再接入响应式链
Mono<Void> toV2 = Mono.fromFuture(
kafkaTemplate.send("eventd-raw-v2", event.userId(), json)).then();
// 按 userId 做粘性灰度:同一个用户只落一侧。
// 按自增计数器取模会让同一个用户的事件随机进两条线,面板数据对不上
if (Math.floorMod(event.userId().hashCode(), 10) != 0) {
return toV2;
}
// v2 的 Event 没有 source 字段,但 9.1 的构造器默认就是 "eventd-v2",
// 这里不需要再补;清洗后 9.2 的 normalize 会把它改成 "eventd-v3"。
// 如果确实要标记「经灰度通道进入」,用 metadata 加业务字段,
// 不要复用 source——它表示的是来源系统,不是链路
return Mono.fromFuture(
kafkaTemplate.send("eventd-raw-v3", event.userId(), json)).then();
}
}
这段代码是真正的分流:90% 用户只写 eventd-raw-v2,10% 用户只写 eventd-raw-v3。不要写成「先写 v2、再给 10% 额外写 v3」——那样 v2 仍处理 100% 流量,v3 只是影子流量,按用户切面板时 90% 和 10% 的边界并不存在。
代价是 v2 面板在这 10% 用户上会缺数据,面板侧必须按 userId 路由到对应版本,或者直接以 v3 面板为准。灰度比例按 userId 哈希固定还带来一个好处:回滚时把哈希为 0 的用户切回 v2 即可,受影响用户的范围是确定的,不用重新洗牌。
数据迁移
v2 的历史数据不需要迁移到 v3:
v2 数据库:保留历史数据,提供查询 API
v3 Redis:只存储实时数据(TTL 5 分钟)
v3 面板:只展示实时数据
如果用户需要历史趋势:
→ v3 面板调用 v2 API 获取历史
→ 或者 v3 的 Flink 作业同时写入长期存储(如 HDFS)
回滚策略
如果 v3 出现问题:
Step 1: 停止向 "eventd-raw-v3" 发送事件
→ v2 恢复 100% 流量
Step 2: v3 Flink 作业保持运行(为了排查)
或 Cancel 从 Savepoint
Step 3: 面板切回 v2
→ 用户无感知(因为 v2 一直在运行)
Step 4: 修复 v3,重新灰度
演进时间线
Week 1: v3 开发完成,内部测试
Week 2: 5% 灰度,对比数据
Week 3: 20% 灰度,监控告警
Week 4: 50% 灰度,问题修复
Week 5: 100% 切换,v2 只读
Week 6: v2 下线(可选)
小结
| 阶段 | 策略 |
|---|---|
| 格式兼容 | 新增字段给默认值;timestamp → eventTime 改名需下游显式兼容 |
| 并行运行 | 双线 Topic,灰度比例控制 |
| 数据验证 | 对比 v2/v3 指标,误差 < 1% |
| 回滚 | 切回 v2 流量,v3 保留用于排查 |
第 14 章结束。进入附录:附录 A Flink DataStream API 速查 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。