关于 mz-debug-freshness
mz-debug-freshness 是用于诊断 Materialize 数据新鲜度问题的技能,按延迟、状态和执行计划逐步定位原因。它要求报告指出具体对象、证据以及相关 SQL 所在位置。
主要功能
- 01汇总当前延迟与近期峰值
- 02沿依赖关系追踪延迟来源
- 03检查摄取状态与水合进度
- 04分析副本负载及算子成本
- 05将执行计划映射回 SQL
适用场景
- 01查明导致延迟的源或汇
- 02区分持续延迟与已恢复峰值
- 03定位副本资源争用或算子倾斜
- 04确定需要修改的视图及 SQL
MCP 相关
该技能使用 Materialize developer MCP server 提供的只读工具:query_system_catalog 和 query,分别查询系统目录以及执行 EXPLAIN 和 introspection 查询。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ materializeinc/ agent-skills --skill mz-debug-freshness安装后在 Claude Code 中输入/mz-debug-freshness确认已加载。
附带文件references/attribution.mdreferences/dataflow-analysis.md
详细介绍
调试新鲜度
找出导致新鲜度问题的那个对象。说明它为什么落后。 报告必须指出问题对象、展示证据,并指出导致昂贵工作的 SQL。
工具
此技能运行在 Materialize 开发者 MCP 服务上,该服务提供两个
只读工具。query_system_catalog 读取 mz_*、pg_catalog 和
information_schema。query 接受一个集群,并运行其他所有内容:
EXPLAIN 语句和 mz_introspection 关系。
客户端配置会在工具名称前加上服务名称,例如 mcp__<server>__query_system_catalog。
references/dataflow-analysis.md 中列出了
EXPLAIN 命令、其输出列以及运算符模式。
下面的每个步骤都会指明所需的确切命令。
工作流
digraph freshness {
"Trigger: something is behind" [shape=ellipse];
"Gather: ranking, status sweep, recent peaks" [shape=box];
"What does the evidence show?" [shape=diamond];
"Report: unhealthy source or sink" [shape=doublecircle];
"Report: peak already recovered" [shape=doublecircle];
"Report: nothing is behind" [shape=doublecircle];
"What type is the object?" [shape=diamond];
"Does one hop carry most of the lag?" [shape=diamond];
"Is the dataflow hydrated?" [shape=octagon, style=filled, fillcolor=red];
"Report: still hydrating, re-run later" [shape=doublecircle];
"Report: lag enters at the root" [shape=doublecircle];
"Does one dataflow dominate the replica?" [shape=diamond];
"Is the load spread across dataflows?" [shape=diamond];
"Report: replica undersized for its workload" [shape=doublecircle];
"Report: inconclusive, list ruled out" [shape=doublecircle];
"Is one worker carrying the operator?" [shape=diamond];
"Report: culprit operator and SQL" [shape=doublecircle];
"Trigger: something is behind" -> "Gather: ranking, status sweep, recent peaks";
"Gather: ranking, status sweep, recent peaks" -> "What does the evidence show?";
"What does the evidence show?" -> "Report: unhealthy source or sink" [label="status not running, Step 4"];
"What does the evidence show?" -> "What type is the object?" [label="an object is behind now"];
"What does the evidence show?" -> "Report: peak already recovered" [label="only a past peak, Step 2"];
"What does the evidence show?" -> "Report: nothing is behind" [label="all three clean"];
"What type is the object?" -> "Report: unhealthy source or sink" [label="table or source, Step 4"];
"What type is the object?" -> "Does one hop carry most of the lag?" [label="view, index, sink, Step 3"];
"Does one hop carry most of the lag?" -> "Report: lag enters at the root" [label="no"];
"Does one hop carry most of the lag?" -> "Is the dataflow hydrated?" [label="yes, Step 5"];
"Is the dataflow hydrated?" -> "Report: still hydrating, re-run later" [label="no"];
"Is the dataflow hydrated?" -> "Does one dataflow dominate the replica?" [label="yes, Step 6"];
"Does one dataflow dominate the replica?" -> "Is one worker carrying the operator?" [label="yes, Step 7"];
"Does one dataflow dominate the replica?" -> "Is the load spread across dataflows?" [label="no"];
"Is the load spread across dataflows?" -> "Report: replica undersized for its workload" [label="yes"];
"Is the load spread across dataflows?" -> "Report: inconclusive, list ruled out" [label="no"];
"Is one worker carrying the operator?" -> "Report: culprit operator and SQL" [label="skew"];
"Is one worker carrying the operator?" -> "Report: culprit operator and SQL" [label="expensive plan"];
}
步骤 1:收集证据并选择调查对象
选择调查对象之前,先运行全部三项查询。
- 当前延迟 对当前落后的对象进行排名。
- 状态扫描 能发现排名无法捕捉的情况:停滞的源会保持当前写入前沿,并报告数秒的延迟,因此在排名中看起来状态正常。此处的任何一行都是一项发现,无论排名结果如何;其错误文本的优先级高于延迟。
- 近期峰值 涵盖过去 24 小时。排名只描述当前这一刻,因此一小时前已恢复的峰值不会在排名中留下痕迹。
结合查看这三项结果:
- 状态扫描中有行,就转到步骤 4,无论其他情况如何。
- 排名中有一个对象明显突出,它就是调查对象。继续执行下文。
- 排名没有明显差异,状态扫描也没有异常,但某个对象的近期峰值远高于其平均值,说明事件已恢复。报告这一点,并附上步骤 2 中的形态以及 计算门控 中的任何重启信息。
- 三项结果都正常,说明没有对象落后。请明确说明这一点。
下游所有查询都以其返回的完整限定名称 database.schema.object
为依据,绝不使用 ID。参考查询都包含一个
materialize.public.my_view 占位符。请替换为你正在调查的名称,否则查询结果为空,对象看起来也会处于空闲状态。参见
名称解析。
type 列决定后续路径:
| 类型 | 路径 |
|---|---|
table、source | 步骤 4。这些对象不会出现在归因追踪中。 |
materialized-view、index | 步骤 2。 |
sink | 步骤 2,然后查看 sink 状态。无法对 sink 进行运算符分析。 |
步骤 2:查看延迟历史
单次读数无法区分突发峰值和持续性问题。
mz_wallclock_global_lag_recent_history 会保留过去 24
小时内每分钟一个样本。
延迟不断攀升、停留在平台期,以及出现单次峰值后恢复,是三种不同的问题。前两种值得继续调查。样本数少于 1440,也代表对象的年龄(分钟数);步骤 6 会用到这一信息。
步骤 3:将延迟归因到某个环节
延迟会沿链路传递,mz_internal.mz_materialization_lag 会对其进行归因。
| 列 | 含义 |
|---|---|
local_lag | 此对象在其直接输入之上增加的延迟。 |
global_lag | 相对于其根输入的延迟。 |
slowest_local_input_id | 此对象最慢的直接输入,沿链路逐环节追踪。 |
slowest_global_input_id | 此对象最慢的根输入,可一次跳转到达。 |
比较各个环节。承载对象大部分 global_lag 的环节造成了延迟,也是步骤 4 及之后要调查的对象,而不是你最初选定的对象。如果没有哪个环节明显突出,说明延迟是从根节点进入的:继续步骤 4,并调查根表或源。
步骤 4:摄取
延迟始于某个表或数据源。
- 状态和错误文本:摄取。如果数据源停滞且存在错误,这就是完整的答案。
- 快照进度,同一章节。
snapshot_committed为 false 表示数据源仍在执行初始快照,并未落后。 - 写入前沿,同一章节,用于查看前沿是否在推进。前沿持续推进并不能排除数据源问题:状态和错误文本的优先级更高。
对于 sink,请查看 sink 状态。
这三项中的任意一项都可能构成完整答案。若是如此,请报告该项并停止排查。
如果数据源的错误文本指出了上游问题,例如不兼容的 schema 变更、发布被删除或凭证过期,那么问题已确诊。修复点位于 Materialize 上游,不在此技能的范围内。
步骤 5:确认水合状态并选择一个副本
执行任何 EXPLAIN ANALYZE 前,先检查两项。
水合状态。正在水合的数据流正在处理快照,因此 EXPLAIN ANALYZE 衡量的是追赶进度,而不是维持进度的能力。如果 mz_internal.mz_hydration_statuses 报告对象尚未水合,请报告这一点,并要求在水合完成后重新运行。
副本。每个副本执行的工作都相同,因此任选一个即可代表。若有两个副本,分析工作量较少且近期没有 offline 记录的那个。
步骤 6:找出消耗副本资源的对象
数据流共享副本的 CPU 和内存,因此,落后的对象可能是被另一个无关对象挤占了资源。
运行 EXPLAIN ANALYZE CLUSTER CPU, MEMORY,然后再运行一次并加上 WITH SKEW。详情:集群级别。
total_elapsed 是自数据流启动以来的累计值,因此,在得出结论前,请将候选对象与其在对象运行时长中显示的年龄进行比较。object 列是后续要追踪的名称;global_id 是临时值,无法用于查找。
将排名与排名本身进行比较。无论它是不是你最初关注的对象,占用副本大部分累计资源的数据流都是步骤 7 的排查对象。它与排名紧随其后的数据流之间的差距,证明了它为何突出。
如果没有对象特别突出,且副本已无剩余余量,则说明工作负载超出副本的承载能力:许多数据流共享该副本,但没有任何一个单独出错。这就是诊断结果,而利用率数据就是证据。如果没有对象特别突出,且副本仍有余量,则结果尚无定论。请报告已排除的可能性,不要将排名第一的对象直接认定为罪魁祸首。
步骤 7:找出开销高的算子
对该数据流的算子进行排序: 算子开销。它接收全限定名称,并在数据流自身所在的集群上运行。
如果某个算子占据了大部分 percent_of_dataflow,就从它入手。列表中的数值逐渐下降,说明开销分散,没有哪个单一算子是答案。应如实说明这一点,而不是只指出开销最大的算子。
精简的结果有两种解读方式,算子名称可以帮助区分。如果一两个算子有实际运行时间,说明这是一个索引,其数据流只负责整理另一个对象的输出:在步骤 8 中找出它索引的对象,并改为分析该对象。如果只有一个算子且开销为 null,说明这是一个不拥有任何算子的直通算子:实际工作位于其 Read 或 Arranged 行所指名的对象中,该对象可能位于另一个集群,因此将该名称带回步骤 6。
只检查开销最高的算子的分布情况: 算子倾斜。
max_worker_ratio 趋近于工作器数量,意味着有一个工作器在承担该算子的工作,数据流的运行速度受该工作器限制。指出该算子,然后检查它是否是一个键为空的 Arrange。空键会将每条记录都路由到同一个工作器;交叉联接或谓词全是不等式的联接都会出现这种情况。联接至少需要一个等式才能分散工作。若有人询问热点键,请说明其特征。
开销高的算子若比率接近 1,说明计划本身开销高,而不是存在倾斜。检查联接顺序,以及可以进一步下推的筛选条件和投影。物理计划的 Source 部分列出了 project、filter 和 pushdown。
算子含义:解读算子。
步骤 8:将算子映射回 SQL
- 在步骤 7 中,你已获得开销高的算子及其
lir_id值。 - 找出哪个对象拥有该 SQL。对于物化视图,对象本身就是 SQL 所属对象。对于索引,通过
mz_indexes.on_id找出它所索引的对象:物化视图上的索引继承该视图的延迟,因此应改为分析物化视图;而普通视图上的索引会在自身数据流中包含整个视图主体。 - 找出内联到该数据流中的视图。普通视图没有自己的数据流,因此其主体会在使用它的对象中构建,开销高的算子可能就来自那里。递归遍历
mz_object_dependencies,收集类型为view的所有依赖项;物化视图、表或源则是拥有自身数据流的边界。查询方式见参考资料。 - 运行
EXPLAIN PHYSICAL PLAN WITH (node identifiers) AS TEXT FOR ...。每个节点都有// { node_id: LirId(N) },与步骤 1 中的 ID 相匹配,还会列出列名、联接谓词和分组键。 - 使用
SHOW CREATE MATERIALIZED VIEW或SHOW CREATE VIEW,查看步骤 2 和 3 中的定义,并找出计划节点对应的子句。
查询和完整示例: references/dataflow-analysis.md。
步骤 9:报告
填写每个字段。“无”是有效值,而且比省略更有用。
**Culprit:** <fully qualified name> on cluster <cluster>, replica <replica>
**Classification:** unhealthy source or sink | recovered peak, nothing behind now
| still hydrating | starved by another dataflow | worker skew | expensive plan |
undersized replica | lag enters at the root | nothing behind | inconclusive
**Evidence:**
- Lag: <object> at <lag>, history <shape> over <samples> minutes
- Attribution: <hop chain, with the local_lag of the hop that owns it>
- Replica: <cpu_percent>, <heap_percent>, top dataflow <percent of total>
**Expensive operator:** <operator> at <percent_of_dataflow>, worker ratio <ratio>
**SQL:** <clause>, in <fully qualified view name>, inlined into <dataflow name>
**Ruled out:** <what was checked and found healthy>
**Unexplained:** <what the evidence does not account for>
如果子句、包含该子句的视图和数据流各不相同,请分别指出,让读者既能看到需要修复的数据流,也能看到需要编辑的文件。
报告是最终交付物。补救措施属于单独的请求。