模式演化
更改事件模式可能破坏依赖它的数据发送程序、查询、仪表板、告警和导出。Telemetry 接受新的 JSON 字段,无需预先迁移。但更改现有字段的类型或含义时,仍需制定计划,处理使用旧定义的代码和已存储的行。
保持现有字段的含义和类型不变。需要改变其中任意一项时,添加新字段或版本。
兼容性矩阵
| 提议的改变 | 摄入兼容性 | 查询兼容性 | 推荐方法 |
|---|---|---|---|
| 添加可选字段 | 通常兼容 | 旧行没有返回值 | 添加、衡量采用情况,然后更新消费者 |
| 添加嵌套对象 | 通常兼容 | 旧行没有嵌套路径 | 保持每个嵌套路径的键入和稳定 |
| 添加受控状态值 | 数据类型兼容 | 详尽的过滤器可能会漏掉它 | 发布前更新并测试消费者 |
| 停止发送可选字段 | 行可以省略 | 消费者看到缺失值 | 首先弃用并衡量剩余读者 |
| 重命名或移动字段 | 创建一个不同的字段 | 老消费者继续看旧名字 | 双写、迁移、然后退出 |
| 将数字更改为字符串 | 与既定类型不兼容 | 计算不再只有一种类型 | 创建一个新的输入正确的字段 |
| 更改单位而不重命名 | 类型可能仍然匹配 | 结果悄无声息地变得错误 | 添加特定于单位的字段,例如 _ms |
| 更改事件粒度 | 行仍在摄取 | 计数和连接变得无效 | 发布新的事件名称或主要版本 |
添加数据在技术上很容易。兼容性还取决于每个下游定义,尤其是受控值、单位、行粒度、标识和时间语义。
添加字段而不破坏消费者
假设api_request_completed已经记录:
{
"event_id": "evt_api_01",
"route": "/v1/query/:id",
"status_code": 200,
"latency_ms": 184,
"release": "2026.07.2"
}
您想要添加有界故障类别:
{
"event_id": "evt_api_02",
"route": "/v1/query/:id",
"status_code": 503,
"latency_ms": 921,
"release": "2026.07.3",
"error_type": "upstream_unavailable"
}
分阶段部署:
- 记录允许的值、隐私类别、所有者和设置字段的分支。
- 添加一个成功的夹具,无需
error_type和每个预期的故障类别。 - 释放生产者,而现有查询仍然忽略该字段。
- 按版本和状态衡量现场覆盖范围。
- 仅在足够的相关行包含仪表板和告警后才更新仪表板和告警。
- 至少在保留的迁移窗口内保持查询对旧行的容忍。
日志 API 删除空值、空对象和空数组。因此,“不存在”是没有值的可选字段的预期存储状态。
在依赖领域之前衡量采用率
使用发布版或显式生产者版本来查找部分迁移的代码:
SELECT
release,
COUNT(*) AS failed_requests,
SUM(CASE WHEN error_type IS NULL THEN 1 ELSE 0 END) AS missing_error_type,
100.0 * SUM(CASE WHEN error_type IS NULL THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0) AS missing_rate_pct
FROM api_request_completed
WHERE status_code >= 500
AND timestamp_utc >= now() - INTERVAL '24 hours'
GROUP BY release
ORDER BY release;
请勿使用 COALESCE(error_type, 'none'),除非“无”是每个旧值和缺失值的预期类别。它可以隐藏损坏的生产者推出。
重命名、移动或重新定义字段
将 latency_ms 重命名为 duration_ms 不是存储事件数据中的就地重命名。使用双写迁移:
{
"latency_ms": 184,
"duration_ms": 184,
"schema_version": 2
}
在迁移窗口期间,明确优先级:
SELECT
route,
approx_percentile_cont(
COALESCE(duration_ms, latency_ms),
0.95
) AS p95_duration_ms
FROM api_request_completed
WHERE timestamp_utc >= now() - INTERVAL '7 days'
GROUP BY route;
然后:
- 更新每个保存的查询、仪表板、告警、导出和消费者;
- 验证当前生产者没有只发送旧字段;
- 等待约定的兼容期;
- 停止写入旧字段;
- 记录保留的旧行的历史查询行为。
当查询必须区分定义时,请使用 schema_version,而不是作为稳定字段名称的替代品。当行粒度、结果含义或嵌套结构一起更改时,版本特别有用。
切勿就地更改字段类型
这种改变是不安全的:
{ "account_id": 8421 }
{ "account_id": "acct_8421" }
第二个生产者与第一个生产者建立的数字 account_id 发生冲突。即使存储层可以分别表示这两个值,连接和过滤器也将不再共享一种可靠的类型。
添加新的字符串字段(例如 account_key),仅在您具有经过审查的确定性映射时才回填,并迁移使用者。同样的规则适用于:
- 作为格式化字符串发送的数字持续时间;
- 布尔值替换为
"yes"和"no"; - 时间戳被特定于语言环境的字符串替换;
- 一个从对象变为标量的嵌套路径;
- 标识符从一种实体类型更改为另一种实体类型。
将状态值视为模式
将 cancelled 添加到之前记录为 success 或 failed 的字段不会更改其字符串类型,但仍然会破坏逻辑:
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)
该查询静默地将 cancelled 视为未失败。在发出新值之前确定它是否属于失败、排除或单独的结果。在查询注册表中搜索详尽的状态过滤器并首先更新装置。
验证推出和回滚
生产前测试代表性事件:
| 夹具 | 它证明了什么 |
|---|---|
| 旧版本成功 | 现有的行和查询仍然有效 |
| 新版本成功 | 添加的字段具有预期的类型 |
| 新版本失败 | 仅错误字段存在且有界 |
| 缺少可选字段 | 空处理仍然是有意的 |
| 重试或重复 | 计数保留记录的颗粒 |
| 回滚生产者 | 较旧的部署可以安全共存 |
部署完成后,按版本比较接受的事件量、必填字段覆盖率、受控值分布以及关键查询结果。仅当旧生产者仍然可以写入已建立的模式并且新消费者容忍其丢失的字段时,回滚才是安全的。
历史数据和回填
图式演化改变了未来的事件;它不会自动重写保留的历史记录。回填前:
- 定义事实的确切来源和确定性转换;
- 保留原始事件时间和稳定的标识符;
- 防止事件或迁移标识符重复;
- 在有限的时间间隔内测试行计数和聚合总数;
- 记录哪些日期和版本被重写;
- 决定仪表板是否应显示混合或回填历史记录。
如果旧数据不能支持新含义,则将其遗漏并显示覆盖范围边界。发明一个值会产生更清晰的图表,但分析的可信度较低。
架构更改清单
- 说明当前和提议的行粒、类型、单位和含义。
- 库存生产者和每个下游查询、仪表板、告警和导出。
- 更喜欢加法领域;使用新事件或版本进行粒度更改。
- 添加成功、失败、缺失、重试和回滚固定装置。
- 当两者都必须更改时,请在新写入器之前部署兼容的读取器。
- 通过发布来衡量采用情况,而不是假设已完成部署。
- 保留记录的双读或双写窗口。
- 仅在了解使用情况和保留历史记录行为后才删除旧路径。
继续使用 事件数据类型与可空性、查询嵌套JSON、事件模式目录 和 必填字段空率配方。