@fx-data/ai-diagnostics 0.1.20261111030608 → 0.1.20261111112203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_SETUP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FX 数据诊断:Agent 安装与管理
2
2
 
3
- 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261111030608`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
3
+ 本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261111112203`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
4
4
 
5
5
  用户明确要求安装、更新、回退、卸载或检查 FX 数据诊断时,也会触发薄 Skill,并直接执行对应管理命令,不先运行 `context` 或读取诊断规则;只有开始诊断时才按薄 Skill 加载内容。普通聊天不主动加载。
6
6
 
package/README.md CHANGED
@@ -20,6 +20,8 @@
20
20
 
21
21
  也可以提供更新的 dagId,询问这次更新为什么慢;或附上 Metric 文件,分析耗时和内存。描述问题时带上“用 FX 数据诊断”,也可以在技能列表中选择“FX 数据诊断”。
22
22
 
23
+ 要一份单租户更新报告,可以说「用 FX 数据诊断分析 corp-xxx 租户近 3 天的更新情况,生成 HTML 报告」。
24
+
23
25
  ## 更新
24
26
 
25
27
  每次开始新诊断时,工具会在后台检查更新,不等待更新即可排查。更新成功后,各 Agent 的下一次新诊断使用新版;当前调查保持原版本,更新失败也不影响排查。Agent 会随后续使用提示更新状态,没有新版时不打扰你。
@@ -38,4 +40,4 @@
38
40
 
39
41
  ---
40
42
 
41
- **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261111030608/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
43
+ **给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261111112203/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
package/content/AGENTS.md CHANGED
@@ -27,9 +27,10 @@
27
27
 
28
28
  | 用户目标 | 起点 |
29
29
  |---|---|
30
+ | 明确要求把单租户一段时间内的更新情况做成报告或 HTML | [更新知识 · 企业更新分析](knowledge/update/workflow.md#企业更新分析) |
30
31
  | 慢、超时、卡顿、内存超限、OOM、`MemoryLimitExceeded`、资源消耗异常、性能退化或回归 | [性能排查入口](knowledge/common/workflow.md),再按场景进入对应知识域 |
31
32
  | 只想拿到或看懂一份 engine metric | [引擎知识](knowledge/engine/workflow.md) |
32
- | 只想弄清更新来源、request、chain/DAG、node attempt、requeue、load、listener、engine task,或核对更新计数 | [更新知识 · 解释更新记录](knowledge/update/workflow.md#解释更新记录) |
33
+ | 询问或分析租户近期更新情况,或解释更新来源、request、chain/DAG、node attempt、requeue、load、listener、engine task 和计数 | [更新知识 · 解释更新记录](knowledge/update/workflow.md#解释更新记录) |
33
34
  | 自动化有没有跑、为什么没触发、通知没收到、回填失败,或给了 workflowId 查执行情况 | [自动化知识](knowledge/automation/workflow.md) |
34
35
  | 只想配置诊断环境,或实时查询被工具、连接问题挡住 | [配置 SigNoZ MCP](knowledge/common/topics/setup-signoz-mcp.md),弄好后回到原问题 |
35
36
  | 其他问题:非性能报错、结果异常、稳定性问题 | 有报错原文就先查[信号索引 · 常见报错](knowledge/signal-index.md#常见报错);没有专门入口,先说明这块覆盖有限,再按[问题定位方法](docs/architecture.md)结合已有材料分析 |
package/content/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  | 组件或仪表板加载慢 | 查询条件、筛选、参数、联动与组件取数 |
11
11
  | 更新慢、超时、内存超限 | 调度等待、依赖、计算、写入与重试 |
12
12
  | 更新来源或数量不清楚 | 触发来源、更新链路、节点与尝试次数 |
13
+ | 生成单租户更新报告 | 小时峰值、`UpdateScene`、执行规模和耗时,交付 HTML |
13
14
  | 自动化没触发、通知没收到或回填失败 | 按 workflowId 核对每次执行、跳过原因、动作结果与回填进度 |
14
15
  | 其他报错、结果异常或偶发不可用 | 核对现象与执行记录,整理已确认事实和待验证问题 |
15
16
 
@@ -33,6 +34,8 @@ npm 安装:对 Agent 说“帮我安装并启用 FX 数据诊断,npm 包是
33
34
 
34
35
  > 企业 corp-xxx,帮我看一下最近的更新情况。
35
36
 
37
+ > 分析 corp-xxx 租户近 3 天的更新情况,生成更新报告。
38
+
36
39
  > 客户怀疑重复更新,请根据这些日志区分更新请求、节点和重试次数。
37
40
 
38
41
  > 帮我看下 workflowId=xxx 这个自动化最近的执行情况,客户说昨晚没收到通知。
@@ -33,6 +33,8 @@
33
33
 
34
34
  ## 沿实际操作还原过程
35
35
 
36
+ 用户明确要求生成单租户更新报告或 HTML 时,按[企业更新分析](../knowledge/update/workflow.md#企业更新分析)聚合窗口、按需下钻并生成文件。一般的更新情况分析按[解释更新记录](../knowledge/update/workflow.md#解释更新记录)在对话中交付。
37
+
36
38
  先说清这次算了什么,再解释哪里慢、为什么失败。按[场景排查](scenario-guide.md)核对:读了哪些表和数据范围;哪些筛选、参数、聚合、联动、权限参与了计算;慢在等待、计算、写入还是返回;有没有失败重试,多了多少工作。
37
39
 
38
40
  页面配置说明可能怎么算,请求和执行记录说明实际怎么算,两者对得上同一对象、同一次操作才能合起来解释。组件取数慢和更新慢同时出现时,要确认这次组件请求是不是在等或读这次更新的结果,共享底层计算只是线索。仪表板内组件异常先按[组件取数对照](../knowledge/dashboard/workflow.md#组件取数先做对照)看单独预览,没有对照也继续分析。
@@ -42,6 +42,9 @@
42
42
  | 仪表板配置影响范围与“绑定下移”核对 | [仪表板 · 核对配置影响范围](../knowledge/dashboard/conclusions.md#核对配置影响范围) |
43
43
  | 企业更新与 dagId 的检索窗口、降级和时间扩展 | [更新发现指引 · 检索时间](../knowledge/update/topics/discover-executions.md#检索时间) |
44
44
  | 按输入选择更新取证配方 | [更新 · 选哪个配方](../knowledge/update/queries.md#选哪个配方) |
45
+ | 企业更新负载的首批统计、租户归属、多日同期对比与画像总结 | [企业更新负载分析](../knowledge/update/topics/enterprise-analysis.md) |
46
+ | 企业更新报告的 JSON 格式和 HTML 渲染 | [企业更新报告交付](../knowledge/update/topics/enterprise-report.md) |
47
+ | 定时同步组释放与回调归并的判读 | [定时同步与回调更新模式](../knowledge/update/topics/datasource-update-patterns.md) |
45
48
  | 更新的计数单位 / 允许的关联边 | [更新 · 计数单位与身份](../knowledge/update/concepts.md#计数单位与身份)、[关联边](../knowledge/update/topics/correlations.md#关联边) |
46
49
  | 更新记录的计数对账、判断矩阵与停止规则 | [更新 · 能下什么结论](../knowledge/update/conclusions.md) |
47
50
  | 按 workflowId 查自动化执行情况 | [自动化 · 按 workflowId 查](../knowledge/automation/queries.md#按-workflowid-查) |
@@ -53,12 +53,17 @@
53
53
  | `dispatch_get_ancestors`、`dispatch_get_descendants` | 查祖先、查下游 | [logical-dependency-expansion](update/topics/events-intake.md#logical-dependency-expansion) |
54
54
  | `node-task-queue receive` | 主队列投递一次,就是一次消费尝试 | [attempt-consumed](update/topics/events-execution.md#attempt-consumed) |
55
55
  | `update` | 消费一次投递的根 span,另开独立 trace;节点结果属性写在它上面 | [attempt-consumed](update/topics/events-execution.md#attempt-consumed)、[primary-node-result](update/topics/events-outcome.md#primary-node-result) |
56
- | `slow-node-task-queue receive` | 租户并发超限,消息转到慢队列 | [capacity-deferred](update/topics/events-execution.md#capacity-deferred) |
56
+ | `slow-node-task-queue receive`、`receive executeInfo at slow queue` | 慢队列消费 span 与日志 | [capacity-deferred](update/topics/events-execution.md#capacity-deferred) |
57
57
  | `dispatch_node_task_exec` | 节点真正执行的区间,加载日志的 `span_id` 指向它 | [一次执行的 span 结构](update/topics/lifecycle.md#一次执行的-span-结构) |
58
58
  | `trigger_listener_<监听器名>` | 监听器回调,一个节点里同名出现 3 次 | [listener-invocation](update/topics/events-outcome.md#listener-invocation) |
59
59
  | `dispatch_callback_children`、`evaluator_child` | 释放子节点、判断子节点能否执行 | [child-release](update/topics/events-outcome.md#child-release) |
60
60
  | `DataFlowExecutor` | 引擎写入(更新的物理写入),其下是 `execute` | [engine-handoff](update/topics/events-outcome.md#engine-handoff) |
61
+ | `write.metric.received`、`write.metric.persisted`、`write.metric.output_data_mb` | 引擎写入接收行数、实际写入行数和输出量 | [写入指标](update/topics/events-outcome.md#写入指标) |
62
+ | `ResourceGroupQueueTimeoutException` | 引擎资源组排队超时 | [engine-handoff](update/topics/events-outcome.md#engine-handoff) |
61
63
  | `dagId` | 更新链 ID | [先记住的几个概念](update/topics/lifecycle.md#先记住的几个概念) |
64
+ | `bundleId`、`groupId` | 同步组与同步任务的标识,按出现的服务和消息区分 | [同步组与任务标识](update/topics/datasource-update-patterns.md#同步组与任务标识) |
65
+ | `executeByConnection`、`syncGroupInterval`、`sync record sent`、`merge sync to engine` | 定时触发、表同步终态与合批提交信号 | [定时同步取证](update/topics/datasource-update-patterns.md#定时同步取证) |
66
+ | `push to push-back mongo success`、`call back ids` | 回调待派发记录与更新提交信号 | [回调取证](update/topics/datasource-update-patterns.md#回调取证) |
62
67
  | `loadData`、`loadDataSkipReason`、`dataModify`、`execution.mode` | 这次有没有加载、为什么没加载、数据是否变化、写入方式 | [load-decision](update/topics/events-outcome.md#load-decision) |
63
68
  | `rowCount`、`updateType`、`rootErrorId`、`dispatchConsumed`、`businessConsumed` | 节点结果属性(行数、更新场景、失败根表、两段耗时) | [primary-node-result](update/topics/events-outcome.md#primary-node-result) |
64
69
  | `merge_count`、`wait_merge_time` | 合并了多条明细时才有 | [merge-decision](update/topics/events-execution.md#merge-decision) |
@@ -30,6 +30,9 @@
30
30
  | [topics/events-execution.md](topics/events-execution.md) | 查消费尝试、限流与锁超时重排队、恢复、合并判断的信号时 | 消费、重排队与合并阶段的 7 个事件 |
31
31
  | [topics/events-outcome.md](topics/events-outcome.md) | 查 load、引擎交接、节点主结果、监听器、子节点释放的信号时 | load、引擎与结果阶段的 7 个事件 |
32
32
  | [topics/discover-executions.md](topics/discover-executions.md) | 只有企业 ID 或 dagId、还没定位到更新执行,或要确定企业、dagId 的检索时间窗时 | 从企业或 DAG 定位更新执行;检索时间 |
33
+ | [topics/enterprise-analysis.md](topics/enterprise-analysis.md) | 要总结租户的更新节奏、来源和负载时 | 指标口径、峰值来源、典型表与 Polars 取样、调度与引擎排队 |
34
+ | [topics/enterprise-report.md](topics/enterprise-report.md) | 要交付单租户更新分析报告时 | 固定 JSON 字段、缺失值和 HTML 渲染 |
35
+ | [topics/datasource-update-patterns.md](topics/datasource-update-patterns.md) | 要解释同步相关峰值或判断全量、增量时 | 同步组释放、回调归并和单次同步方式取证 |
33
36
  | [topics/correlations.md](topics/correlations.md) | 核对异步关联或拆分耗时时 | 允许的关联边与 12 个性能片段 |
34
37
 
35
38
  ## 关联
@@ -24,12 +24,12 @@
24
24
  | 一条链的受理和各节点执行 | `name = 'dispatch_submit' AND dagId = '<链 ID>'`;`name = 'update' AND dagId = '<链 ID>'` | `dagId` 只在 `dispatch_submit` 和已执行的 `update` 上有 |
25
25
  | 某张表的消费尝试 | `name = 'node-task-queue receive' AND tableId = '<表 ID>'` | 队列 span 总带 `tableId`、`corpId`,包括限流转发和拿锁失败的投递 |
26
26
  | 某张表的节点执行结果 | `name = 'update' AND tableId = '<表 ID>'`,看 `updateType`、`loadData`、`rowCount`、`rootErrorId` | 没有这些属性的 `update` 没走到节点执行 |
27
- | 某个企业的更新 | 先用 `corpId` 查队列 span 或 `dispatch_submit`,再按 `linked` 找 `update` | `update` 上的 `corpId` 只有执行过的才有,不能用来排除企业 |
27
+ | 某个企业的更新 | 先用 `corpId` 查主队列消费 span,再用显式关联找提交或 `update` | `dispatch_submit` 上的 `corpId` 覆盖须核对,见[提交归属](topics/enterprise-analysis.md#核对提交归属与窗口覆盖) |
28
28
  | 从发布方找到执行方(或反过来) | `name = 'update' AND linked = '<发布方 trace ID>'` | `linked` 保存的是发布方 trace ID |
29
29
  | 某次加载的起止与耗时 | 日志 `body CONTAINS 'table load success'`(或 `table load start`、`table load fail`),带 `trace_id` | 日志的 `span_id` 等于 `dispatch_node_task_exec` |
30
30
  | 某个节点的失败原因 | 日志 `body CONTAINS 'execute task error'`,带 `exception.type`、`exception.message` | 同一次失败还有一条不带堆栈的 `task exec error` |
31
31
  | 某张表有没有引用已有结果、tag 和旧版本释放 | `update` 上 `execution.mode = 'REF'`;日志 `body CONTAINS '<表 ID>'` 加 `[ResultBinding]`、`[Snapshot tag`、`[ResultRetention]` | 见 [result-publish](topics/events-outcome.md#result-publish) |
32
- | 限流和重排队 | 日志 `body CONTAINS 'Tenant limit exceeded'`、`resend execute info` | 慢队列每转一圈打一行,条数不等于节点数 |
32
+ | 限流和重排队 | 日志 `Tenant limit exceeded`、`receive executeInfo at slow queue`、`resend execute info` | 转发、慢队列消费和锁超时重发分别取证。见[限流转发](topics/events-execution.md#capacity-deferred)和[等锁重发](topics/events-execution.md#lock-requeued) |
33
33
 
34
34
  按已有输入选一个配方,只补当前缺的那一项。配方只限定参考范围,不证明每个分支都发生,也不要求跑完全部步骤。事件见[事件索引](signals.md#事件索引),边与片段见[关联边与性能片段](topics/correlations.md)。
35
35
 
@@ -0,0 +1,49 @@
1
+ # 定时同步与回调更新模式
2
+
3
+ 在企业更新趋势中看到 `SCHEDU` 或 `CALL_BACK` 峰值,或需要解释某张表反复被这些来源更新时读这里。先按[企业更新负载分析](enterprise-analysis.md)确定窗口和目标链。以下机制与信号已按源码核对,实际配置、字段覆盖和阶段关联以对应执行记录为准。 [知识适用性](../../common/conclusions.md#知识适用性)
4
+
5
+ ## 同步组与任务标识
6
+
7
+ | 标识 | 在什么范围内使用 |
8
+ |---|---|
9
+ | `bundleId` | 定时同步组 ID。一轮到期计划产生一个批次,可跨连接 |
10
+ | `taskId`、sync 的 `groupId` | server 消息里的 `id`,同步记录里的 `taskId`,同步器代码和 `group task start/complete` 日志里的 `groupId`。标识一次同步任务,可以包含同连接的多张来源表 |
11
+ | DM 通知里的 `groupId` | server 将它作为 `bundleId`,再按连接生成发送给 sync 的任务 ID |
12
+ | `dagId` | 一次分析表更新提交对应的更新链。一个同步组可以分批提交,扫描器也可以把多个组的就绪表合到一条链 |
13
+
14
+ ## 定时同步如何释放
15
+
16
+ 1. 计划时刻先按租户的同步分组间隔对齐,读不到租户配置记录时默认 60 分钟。每轮挑已启用且已到期的计划,分配一个 `bundleId`,再按连接归集表清单。支持表级同步的路径会在组内记录各表的同步结果。同一连接的一份同步任务可以拆成多张来源表分别执行。
17
+ 2. 旧版定时路径等待整组结束再提交成功项。表级扫描在整组结束,或有一条已结束、未派发的表记录等满阈值时选中组。这里的结束包括成功和失败。仓库默认扫描间隔为 10 分钟,等待阈值为 20 分钟,实际以部署配置为准。
18
+ 3. 组一旦被选中,扫描器取该组所有已成功、需要更新且尚未派发的表,可以包含刚成功的表。尚在同步的表留待后续。扫描器按同租户、同场景合并多个就绪组,按连接和来源表找到分析表候选后提交。提前提交后保留已派发标记,整组完成回调补齐剩余符合条件的表并清理记录。
19
+
20
+ ## 定时同步取证
21
+
22
+ | 要区分的现象 | 按需取的证据 |
23
+ |---|---|
24
+ | 配置时刻本身集中,或不同配置时刻对齐后才集中 | 核对客户计划时刻、频率和时区。按 `bundleId` 查 `executeByConnection` 的 `storeId`、`dataSourceType`、`syncGroupInterval`,以及 `begin sync datasource` 的企业、连接、任务和触发时间。`syncGroupInterval` 表示这轮读到的值,实际定时节奏看连续轮次。`begin to execute datasource schedule jobs` 中 `tenantId` 占位符填的是连接 ID,归属用其他明确记录核对 |
25
+ | 表同步集中结束,或结束分散但集中释放 | 用 `sync record sent` 取企业、连接、来源表、`taskId`、`bundleId` 和终态时间。用 `merge sync to engine, dagId = {}, bundleIds = {}` 或其他明确关联找到对应 `dispatch_submit`。扫描日志中的组列表是本轮候选组,逐组贡献需核对实际派发成员 |
26
+
27
+ 比较计划、实际触发、同步结束和提交时间。旧路径等待整组时可能以 `bundleId` 作返回占位,以真实提交记录确认 `dagId`。同表反复作为下游节点出现时,沿实际链路找触发它的来源表。DM 的同步完成通知保留定时或非定时语义,分别可能落到 `SCHEDU` 或 `MANUAL_UPDATE`。简道云系统数据的部分定时路径走 `CDC_SCHEDULE`,API 数据源定时同步完成也可以走 `CALL_BACK`。 [场景入口](scene-producers.md)
28
+
29
+ ## 回调如何归并与派发
30
+
31
+ 简道云 webhook 或 CDC、数知鸟、API 数据源同步完成和本地宝自动更新都可以进入回调派发路径。sync 按回调标识归并待派发项,到达可派发时间后按租户打包。全量 CDC 可以立即标为可派发,由后台扫描发送。server 将回调标识映射到分析表,检查更新状态和来源容量,将符合条件的目标提交成一条 `CALL_BACK` 链。单条回调入口也有更新状态和限频检查。上游通知归并与调度侧节点合并各自判断,`CALL_BACK` 的默认调度合并规则见[更新场景](../concepts.md#更新场景updatescene)。
32
+
33
+ ## 回调取证
34
+
35
+ 对照已确认来源的事件或同步完成时间、`push to push-back mongo success` 上的企业与来源表,以及 server 的 `call back ids {}, chainId {}` 和对应 `dispatch_submit`。这样可区分来源通知集中和待派发项归并后集中送出。来源事件、待派发项、已提交更新链分别计数。
36
+
37
+ `call back ids` 在这条 server 日志里是映射后的分析表 ID。确认回调来自哪张来源表需要明确的映射或同次调用记录。回调成功响应也可能没有对应的更新提交,按实际 `dagId` 与受理记录确认。只有场景和时间分布时,报告看到的集中现象,客户习惯的解释由当期配置或来源事件补证。
38
+
39
+ ## 判断单次同步是全量还是增量
40
+
41
+ 从已确认的更新链回查同步批次,再按连接、来源表和任务核对实际执行。确认到批次时给批次结论,确认了来源与分析表的血缘再给表级结论。来源抽取范围、同步目标覆盖或追加、下游分析表写入规模分别取证。`SCHEDU`、`CALL_BACK` 描述更新来源,`isAllTables` 描述本次选表范围,均不能直接判定行级同步方式。
42
+
43
+ | 来源执行 | 可以核对的信号 |
44
+ |---|---|
45
+ | 云端 JDBC | 将 `CloudJdbcDataReceiver` 的 `jdbc sourceTableId: …, receive finish …` 与同表、同任务的成功 `sync record sent` 对账。此路径每次读取配置 SQL 的结果并覆盖同步目标。日志行数是本次抽取结果,源库扫描范围要看当期 SQL 或源端指标 |
46
+ | DM 数据湖写入 | 有对应 `flushData` 时取 `destinationSyncMode`,结合已关联的覆盖或追加完成日志判定该次目标写入方式。来源抽取范围另看来源执行记录 |
47
+ | 本地宝 JDBC CDC | 以实际执行的变更写入和游标推进确认增量。缺初始 CDC 参数或收到 DDL 时可发起全量同步,需另对到该次同步完成记录 |
48
+
49
+ 观察到反复覆盖时,再核对变化行数与抽取行数的比例、来源支持的水位或 CDC、更新删除语义以及全量初始化或回退场景。满足前提后才给增量方案,复核抽取量、下游写入负载和数据一致性。实际模式证据不足时,将这项检查列为待确认。
@@ -0,0 +1,67 @@
1
+ # 企业更新负载分析
2
+
3
+ 用同一个租户和时间窗看更新链提交、队列消费和节点执行的趋势,再按实际贡献峰值的场景补证。各查询的租户字段、关联和覆盖先用目标环境的小范围记录核对。 [企业更新发现](discover-executions.md#企业入口)
4
+
5
+ ## 输入与时间范围
6
+
7
+ - 用已确认的企业 ID 精确过滤 `corpId`。已有 ID、环境或时间范围直接复用。只有企业名称且无法唯一定位时,追问企业 ID。环境与服务按[环境选择规则](../../common/concepts.md)确定。
8
+ - 用户指定的时间范围优先。缺省按[企业更新检索时间](discover-executions.md#检索时间)查看最近 1 天,也就是滚动 24 小时。如果用户希望判断峰值是否在多天的同一时段重复,可以询问是否扩到最近 3 天或 7 天,确认后再查询。
9
+ - 开始查询时固定绝对时间窗 `[start, end)`。优先使用用户指定或已确认的租户时区,缺省用北京时间并注明。首批按小时聚合,多天对比按相同本地时段对齐。解释选定峰值时再缩小时间粒度。 [公共查询规则](../../common/queries.md#查询约束)
10
+
11
+ ## 首批查询与统计口径
12
+
13
+ 趋势优先用服务端聚合。需要对账或跨 span 关联时,用 Query Builder 点名字段,保留原始时间和 trace/span ID。只有分页和覆盖已核对的原始记录才用于总体统计。 [查询例子](../../common/topics/query-example.md) · [更新计数口径](../conclusions.md#计数对账)
14
+
15
+ | 报告指标 | 首批信号与字段 | 统计口径 |
16
+ |---|---|---|
17
+ | 观测到的更新链 | `dispatch_submit` 的 `dagId`、`updateScene`、`totalSize`、`corpId` | 按提交开始时间分桶,桶内及全窗总量各自按 `dagId` 去重。分场景用原始 `updateScene`。`totalSize` 是链涉及的表规模 |
18
+ | 主队列消费次数 | 主队列 `node-task-queue receive` 的 `corpId`、`tableId` | 按消费开始时间数 span。一次消费计一次,同一明细重新投递后再次消费也计入 |
19
+ | 表更新执行次数 | 归属已核对的 `dispatch_node_task_exec` | 按节点开始时间数 span。每次进入业务执行计一次,同一表可以多次执行 |
20
+ | 表更新业务耗时、执行场景、表结果行数 | 外层 `update` 的 `businessConsumed`、`updateType`、`rowCount`、`tableId`、`tableName`、`dagId` | 有业务耗时值的记录计入分位数和表级频次。按原始 `updateType` 分组统计全窗和峰值,并分别给出该口径的分母。表结果行数取对应更新的有效值 |
21
+ | 写入任务接收行数、引擎上报输出量 | 经核对的更新写入 `execute` 上的 `write.metric.*` | 按写入尝试开始时间分桶,汇总有效指标。过滤条件见[写入规模](#写入规模) |
22
+
23
+ 主队列 span 名可能带部署前缀。短名精确查空时,在同一环境、服务和短时间窗内用 `name CONTAINS 'node-task-queue receive'` 按名称聚合,再选实际的主队列名称。慢队列的消费另计。节点执行 span 能按 `corpId` 归属且与短窗明细对账时,可直接聚合数量。节点 span 和外层 `update` 的耗时字段、起点及覆盖分别核对。
24
+
25
+ ### 写入规模
26
+
27
+ 在目标服务和短窗内,先用实际父子 span 核对 `dispatch_node_task_exec → DataFlowExecutor → execute`,并检查 `corpId` 在写入 span 上的覆盖。条件对账后,在同一服务按租户筛 `name = 'execute' AND write.metric.parsed = true AND write.metric.skipped = false AND write.metric.received > 0`。按需取 `count()`、`sum(write.metric.received)` 和 P50、P95、P99。输出量仅汇总有有效 `write.metric.output_data_mb` 的记录,单列覆盖。`execute` 无法确认属于更新时,只报告可确认的写入范围。 [写入指标字段](events-outcome.md#写入指标)
28
+
29
+ 接收行数会累计重复处理。落盘行数单看有覆盖的 `write.metric.persisted`,报告其有效记录数。无有效值时标明缺少落盘量覆盖。`update.rowCount` 用来描述表结果大小,Polars 样本的扫描与写出行数用来描述该次任务做了多少工作。取 `rowCount` 前确认目标环境的字段类型和存在性,字段未返回时保留未知。 [更新结果字段](events-outcome.md#primary-node-result) · [Polars 读写指标](../../engine/topics/metric-fields.md#读表输出和写表)
30
+
31
+ ## 核对提交归属与窗口覆盖
32
+
33
+ 主队列消费明确写入 `corpId`、`tableId`。`dispatch_submit` 显式写入 `dagId`、`totalSize` 和 `updateScene`,企业标签依赖通用 trace 逻辑补写。先用当前环境样本确认按 `corpId` 直接过滤提交的覆盖,再用已确认的消费、执行或来源记录对账。通过实际父子关系、原生 span link、明细 ID 或明确的 `dagId` 关联。一个 `linked` trace 下有多个候选时,用业务标识消歧。 [关联规则](correlations.md#关联边) · [独立 Trace 的显式链接](../../common/topics/linked-traces.md#独立-trace-的显式链接)
34
+
35
+ 从消费端回查只能发现已观察到消费的链,窗口末尾尚未消费的提交会漏掉,窗口内也可能消费更早提交的链。提交、消费和执行分别按自己的事件时间入桶。归属或覆盖不足时用「已观察到的更新链」,队列消费趋势按自身口径保留。查询限额、分页、保留期和字段缺失分别记录,只有已核对覆盖的空桶才按零值呈现。 [判断覆盖范围](../conclusions.md#选择运行与阶段测量)
36
+
37
+ ## 按现象补查
38
+
39
+ 1. **追峰值来源。** 先比较同一小时的提交链量、场景占比和表更新执行量,再按 `dagId` 或实际关联下钻对峰值贡献大的链。提交用 `updateScene`,执行用外层 `updateType`,各报自己的分母。场景缺失时找同次执行的来源记录,仍缺则保留「场景未知」。没有明确高峰时看整个已覆盖窗口。 [场景入口](scene-producers.md)
40
+ 2. **查同步批次。** `SCHEDU` 或 `CALL_BACK` 成批出现时,可核对配置触发、同步结束、释放和提交的时间。区分同一表作为触发来源或依赖链下游。想判断全量或增量时按来源表、连接及执行记录确认实际模式;只能对到批次时,报告到批次。 [同步组与同步方式](datasource-update-patterns.md)
41
+ 3. **选典型表。** 分别从单次写入规模、执行频次和慢执行中选少量可核对的代表。写入样本从 `execute` 沿父子链回到外层 `update`,补表名、ID、场景和对应结果行数。高频按同口径记录数比较;慢表尽量选同表、同场景的较快执行对照。报告候选来自全窗聚合还是峰值抽样,写清入选理由。
42
+ 4. **拆耗时。** 在相同口径下并列样本数和业务耗时 P50、P95、P99;中位数相近时继续看长尾。对选定样本按精确 task 取少量 Polars Metric,比较读了多少行、做了什么、写出多少、排队和执行耗时。按同一更新对齐重试,逐次记录失败原因、重试间隔及最后一次执行。并行线程的等待只做可比指标,不能相加成任务耗时。 [引擎任务查询](../../engine/queries.md#metric-链接与任务日志)
43
+
44
+ ### 排队发生在哪
45
+
46
+ | 要区分的环节 | 判断依据 |
47
+ |---|---|
48
+ | 调度侧主队列等待 | 对齐同一投递的发布和主队列开始消费时间。主队列消费量反映处理次数,等待时长另算 |
49
+ | 调度侧租户限流转慢队列 | 按租户精确匹配 `Tenant limit exceeded` 日志统计转发次数。选少量执行明细,用 `executeId` 对到慢队列消费和返回主队列。慢队列消费缺租户属性,只有明确归属的记录才能用于租户结论。 [限流转发](events-execution.md#capacity-deferred) |
50
+ | 引擎侧资源组排队或重试 | 对到同一次更新内的引擎尝试,再用引擎任务阶段耗时或同次错误日志确认。 [引擎交接](events-outcome.md#engine-handoff) |
51
+
52
+ 消费次数明显抬升而执行量接近时,先比较同一时段的租户限流转发次数,必要时再查锁与合并。引擎排队和调度侧慢队列可以在同一窗口同时出现,分别报告各自范围。 [更新性能片段](correlations.md#性能片段)
53
+
54
+ ## 画像总结
55
+
56
+ 画像以企业为单位。先用一句话概括窗口里最突出的更新特征,再按下表挑有证据支持的事实。每条结论写出它对应的时段、数量或对照。涉及个人操作时,使用明确的发起人或来源事件记录。
57
+
58
+ | 画像维度 | 怎样从统计结果归纳 |
59
+ |---|---|
60
+ | 更新规模与节奏 | 写出观察到的链量、最集中的时段和对应数量。单日描述当日分布。多日比较相同本地时段,写明峰值在几个可比日期出现过,按实际覆盖区分反复出现和偶发峰值 |
61
+ | 主导来源 | 比较全窗口和峰值时段的场景。并列实际增加的链数、执行数及其分母,保留场景未知的数量 |
62
+ | 任务规模与耗时 | 用有覆盖的结果行数、写入规模和耗时分布描述任务。抽样解释计算或耗时差异时,写出样本日期、场景和对应表 |
63
+ | 关注表与耗时问题 | 分开列值得关注的表和已观察到的耗时问题。表给名称、ID、频次、规模和入选理由。问题给发生位置、时段、表或企业范围、对照证据。样本中未发现明确瓶颈时交代已观察的范围 |
64
+
65
+ 例如「5 个可比日期里有 4 天在 10:00–11:00 出现峰值,增加的链主要是 `SCHEDU`」。要区分计划集中和同步组集中释放,再看对应的计划和释放记录。
66
+
67
+ 最后按影响和证据给建议,写明适用前提及复核指标。比如确认计划配置集中后,可以评估错峰,复核峰时链量、队列等待和数据更新时间。关联或覆盖有缺口时,先交付已确认的画像,说明哪条解释仍待确认。 [诊断结果交付](../../../docs/diagnostic-report.md)
@@ -0,0 +1,63 @@
1
+ # 企业更新报告交付
2
+
3
+ 按[企业更新负载分析](enterprise-analysis.md)取得单租户证据后,将可复核的统计、简短结论和覆盖写成版本 1 JSON,再渲染成单文件 HTML。页面标题为「企业更新负载报告」。按小时的柱子对齐比较日期,点选峰值查看场景和占比。全窗口分别用饼图展示提交链和执行记录的场景构成,按各自覆盖计算占比。完整明细保留原始 `UpdateScene`。报告包含单日画像、高峰来源、任务执行、关注表与耗时问题四块,以及指标口径。
4
+
5
+ ## 固定 JSON 格式
6
+
7
+ 所有字段都要写出,数值用原始单位。未取得的数据用 `null`,覆盖已确认的零值用 `0`。列表无可列项目时用 `[]`。文中已有脱敏规则同样适用于 JSON 和 HTML。范围和摘要使用业务语言,覆盖说明写清统计对象、缺失或仅抽样的范围。
8
+
9
+ | 根字段 | 内容 |
10
+ |---|---|
11
+ | `schemaVersion` | 整数 `1` |
12
+ | `scope` | `tenant` 为可交付的租户标识,`environment` 为环境名,`start`、`end` 为绝对窗口,`timezone` 为展示时区 |
13
+ | `summary`、`coverage` | 一段简短结论、一个覆盖说明字符串数组 |
14
+ | `totals`、`hours` | 全窗统计、按小时的分桶列表 |
15
+ | `sources` | 全窗场景构成和有证据的峰值明细 |
16
+ | `execution` | 执行摘要、分位数和少量关联到同次更新的 Polars 样本 |
17
+ | `attention` | 典型表、耗时问题、可观察范围和下一步核对 |
18
+
19
+ ### 时间桶和场景
20
+
21
+ - 所有时间写 `YYYY-MM-DDTHH:mm:ss+08:00` 这样的带偏移时间,偏移取展示时区在该时点的实际值。范围是 `[start, end)`。`hours` 按实际时间顺序列连续的本地整点桶,覆盖整个窗口。边界只覆盖部分小时用 `partial`,完整小时用 `complete`,无观测的桶用 `missing`。整个趋势均缺时 `hours` 可用 `[]`。
22
+ - `totals` 含 `chains`、`executions`、`queueConsumes`、`receivedRows`、`slowQueueTransfers` 和 `outputMiB`。`hours` 每项是 `{start, coverage, metrics}`,`metrics` 含同样的键,省去 `outputMiB`。前五项是非负整数或 `null`,`outputMiB` 是非负数或 `null`。`missing` 桶的各指标均用 `null`。整窗与分桶按各自覆盖核对,更新链总量按全窗去重。
23
+ - 场景列表的每项是 `{scene, count}`,`scene` 为原始值或 `null`(场景未知),`count` 为非负整数。一份列表里同一场景合并成一项。整组场景统计缺失时,整份列表用 `null`。
24
+ - `sources` 含 `summary`、`chainsByScene`、`executionsByScene`、`peaks`。前两组是全窗聚合,分别按提交和执行口径计算;在饼图展示各自的前五个场景和其余场景,展开可查全量明细。一整组统计未取得时填 `null`,报告在组级说明缺口。峰值每项是 `{label, summary, hours}`,其中 `hours` 每项是 `{start, chainsByScene, executionsByScene}`。`start` 精确引用上面的时间桶。需要对比多日同期时把同一时段的各天整点放入一个峰值。没有明确峰值或缺少峰值来源时 `peaks` 可用 `[]`,渲染时展示全窗场景。
25
+
26
+ ### 执行和明细
27
+
28
+ - `execution` 含 `summary`、`distributions`、`polarsSamples`。分位数每项是 `{metric, scope, sampleCount, p50, p95, p99}`,`metric` 用 `businessMs` 或 `receivedRows`,单位为毫秒或行。`scope` 给本次统计范围,`sampleCount` 给有效记录数。小样本的未展示分位数用 `null`。
29
+ - `polarsSamples` 每项是 `{at, tableName, tableId, scene, calculation, scannedRows, writtenRows, resultRows, businessMs, outputMiB}`。`at` 为样本执行开始时间,`tableName` 或 `tableId` 至少一项有值。能确认什么就填什么。`resultRows` 取对应表结果,扫描和写出取同次 Polars 统计。
30
+ - `attention` 含 `tables`、`latencySummary`、`latency`。关注表每项是 `{name, id, scenes, executionCount, resultRows, resultAt, reason, evidence, nextCheck}`。表名或 ID 至少一项有值,场景沿用场景列表格式。`resultAt` 是该结果观测时间,`reason`、`evidence` 写可核对的入选理由和记录。
31
+ - 耗时问题每项是 `{stage, scope, finding, evidence, limits, timeline}`。`stage` 用 `mainQueue`、`slowQueue`、`engineResourceGroup`、`engineWrite` 或 `other`,对应实际发生位置。`scope` 交代时间和对象。`timeline` 每项是 `{kind, label, durationMs}`,`kind` 用 `failed`、`wait`、`succeeded` 或 `other`,仅在时间线已核对时填。缺少可关联证据时保留聚合,在 `coverage` 或 `limits` 写明范围。只有覆盖支持时才在 `latencySummary` 写「观察范围内未见明确瓶颈」。
32
+
33
+ 以下是缺少小时和关联明细时仍可交付的完整输入。`summary` 及各说明填本次真实观察。
34
+
35
+ ```json
36
+ {
37
+ "schemaVersion": 1,
38
+ "scope": {
39
+ "tenant": "示例租户", "environment": "work", "timezone": "Asia/Shanghai",
40
+ "start": "2026-11-01T09:00:00+08:00", "end": "2026-11-02T09:00:00+08:00"
41
+ },
42
+ "summary": "观察到 2 条更新链。小时分布及执行关联待核对。",
43
+ "coverage": ["仅有可归属的提交汇总。"],
44
+ "totals": {
45
+ "chains": 2, "executions": null, "queueConsumes": null,
46
+ "receivedRows": null, "slowQueueTransfers": null, "outputMiB": null
47
+ },
48
+ "hours": [],
49
+ "sources": {"summary": null, "chainsByScene": [{"scene": "SCHEDU", "count": 2}], "executionsByScene": null, "peaks": []},
50
+ "execution": {"summary": null, "distributions": [], "polarsSamples": []},
51
+ "attention": {"tables": [], "latencySummary": null, "latency": []}
52
+ }
53
+ ```
54
+
55
+ ## 渲染和交付
56
+
57
+ 将 JSON 保存为 `enterprise-update-report.json`。源码目录从仓库根目录运行以下命令,输出目录默认是 JSON 所在目录,也可显式传 `--output-dir`。
58
+
59
+ ```sh
60
+ python3 scripts/render_enterprise_update_report.py /path/to/enterprise-update-report.json
61
+ ```
62
+
63
+ 安装副本用 [固定版本的 Python 入口](../../../AGENTS.md#安装副本的助手规则),把 `python3` 替换为 `<node> <pinned_cli> python`,脚本路径以 `content_root` 为准。调查文件和报告写到会话目录。渲染器只用 Python 标准库。脚本输出 `enterprise-update-report.html` 的路径,同一份 JSON 生成相同 HTML。数据、样式和交互脚本内联,静态预览可离线显示默认柱图、饼图和明细,支持脚本的浏览器可切换指标和日期。格式错误时按报出的字段修正输入。交付时给 HTML 链接和简短结论,交代证据缺口。
@@ -53,7 +53,7 @@
53
53
  - 慢队列只有一个消费者,每收到一条打 `receive executeInfo at slow queue`;额度可用就发回主队列,仍不可用就再发回慢队列,如此循环,所以一条消息在慢队列里可能出现很多次。
54
54
  - 慢队列消费不开 `update` trace,也没有 `corpId`、`tableId`。
55
55
  - 分支:收到关闭信号时把消息重新发回慢队列;其他异常只记 ERROR,不重发。
56
- - 不能说明:慢队列每转一圈是一次重排队,不是一次执行尝试;日志条数多只说明等得久,不说明有多少个不同节点。
56
+ - 慢队列消费日志按每次收到消息计数。用 `executeId` 对齐转发、慢队列消费和后续主队列消费,才能观察同一明细的等待或重复投递。消息次数、等待时长和不同节点数量分别统计。
57
57
 
58
58
  ## lock-requeued
59
59
 
@@ -87,10 +87,24 @@ REF 不写数据,而是选定来源的一个版本,给它打保护 tag,再
87
87
  - 结果里的 `taskId` 会写进节点结果埋点。
88
88
  - 分支:
89
89
  - 可重试的引擎失败:服务端可以重试,每次重试产生新的引擎任务 ID,成功结果只暴露最后返回的那个。
90
+ - 同次更新日志的 `exception.type` 类名为 `ResourceGroupQueueTimeoutException` 时,可以确认该引擎尝试发生资源组排队超时。按 `execute` span 对齐各次尝试,重试间隔另算。
90
91
  - 数据源湖(MPP)写入遇到引擎任务内存不足:允许重试一次,并把流水线并行度降为 1,同时写 `compute.retry.pipeline_parallelism_reduced`。这只针对这一类写入,不是所有 OOM 的通用重试策略。
91
92
  - 引擎写入失败:事务回滚,服务端加载失败;要继续追查,需要具体的引擎任务或请求标识,否则停在服务端边界。
92
93
  - 不能说明:直通、不活跃、不需要加载等没有引擎任务;失败或重试过的任务不会都留下标识,引擎内部细节不在这里。
93
94
 
95
+ ### 写入指标
96
+
97
+ `write.metric.*` 来自引擎返回的写入统计解析,随当前 `execute` span 记录。字段能否用于聚合取决于目标环境和写入路径的覆盖。原始回传在 `write_metric`。
98
+
99
+ | 字段 | 含义 |
100
+ |---|---|
101
+ | `write.metric.parsed`、`write.metric.skipped` | 是否解析成功、引擎是否跳过写入 |
102
+ | `write.metric.received` | 写入器收到的行数 |
103
+ | `write.metric.persisted` | 引擎回报的实际写入行数。负值表示缺少有效计数 |
104
+ | `write.metric.output_data_mb` | 引擎回报的输出大小,按 MiB 记录,负值表示缺少有效计数 |
105
+
106
+ 对照表结果时使用同次更新的 `rowCount`;需要确认引擎任务实际扫描与写出多少时取对应 Polars Metric。 [任务读写规模](../../engine/topics/metric-fields.md#读表输出和写表)
107
+
94
108
  ## primary-node-result
95
109
 
96
110
  节点执行得出的主结果:成功或失败。这一步之后才会通知监听器、释放子节点。
@@ -70,10 +70,9 @@
70
70
 
71
71
  ## SCHEDU
72
72
 
73
- - 定时任务:数据源计划创建一次带定时标记的同步任务。回调处理按表级 bundle 或旧同步任务条件放行,bundle 扫描器可能更早释放已成功的表,不必等全部 bundle 结束。
73
+ - 数据源计划创建带定时标记的同步任务。表级路径可以提前释放已成功的表,旧路径等待整组结束。 [定时同步如何释放](datasource-update-patterns.md#定时同步如何释放)
74
74
  - 消息或回调:DM 完成回调只含定时结果时启动定时同步;只有不支持表级同步的 bundle 走这条旧回调。
75
75
  - 接口请求:DM 表级流状态通知。按 bundle 能力和状态决定是否更新一次:只调度已完成、非重复的工作,未完成的把分组标记为失败。定时标记决定是 SCHEDU 还是 MANUAL_UPDATE。
76
- - 定时任务:同步分组里有可增量释放的成功记录时,收集这些表及子表,按场景分组、跨 bundle 合并后调度;提交成功才删分组记录。扫描器释放和回调完成是两条路径,回调次数不等于更新次数。
77
76
 
78
77
  ## FOLDER_COPY
79
78
 
@@ -144,7 +143,7 @@
144
143
 
145
144
  ## CALL_BACK
146
145
 
147
- - 消息或回调:数据源 webhook 回调报告数据已就绪,把回调标识翻译成分析表的根并调度更新。
146
+ - 来源通知进入回调派发后,按回调标识映射到分析表并提交更新。适用来源、归并和派发条件见[回调如何归并与派发](datasource-update-patterns.md#回调如何归并与派发)。
148
147
 
149
148
  ## ARCHIVE
150
149
 
@@ -1,12 +1,19 @@
1
1
  # 更新 · 排查入口
2
2
 
3
- 分析表更新的记录看不懂,或者更新慢、失败时,从这里下手。常见的目标有两种:
3
+ 分析租户更新、解释分析表更新记录,或者排查更新慢和失败时,从这里下手。
4
4
 
5
- - **解释更新记录**:企业最近的更新情况、更新从哪来、request、chain/DAG、节点尝试、requeue、load、listener、engine task,或者核对计数。把记录里看得到的阶段事实讲清楚就够了,不用下最终根因。
5
+ - **企业更新分析**:用户明确要单租户更新报告或 HTML,例如「分析 corp-xxx 租户近 3 天的更新情况,生成更新报告」。
6
+ - **解释更新记录**:在对话里看企业近期更新情况、更新从哪来、request、chain/DAG、节点尝试、requeue、load、listener、engine task,或者核对计数。按问题解释记录中可见的阶段和事实。
6
7
  - **更新性能排查**:一次更新慢、超时、内存超限、OOM、资源消耗异常或性能回归。这类问题一般从[性能排查入口](../common/workflow.md)进来,在解释更新记录的基础上找出瓶颈阶段。
7
8
 
9
+ ## 企业更新分析
10
+
11
+ - 确认单个租户、环境、时区和时间窗。已有同一范围的分析结果直接复用。按[企业更新负载分析](topics/enterprise-analysis.md)补齐需要的窗口聚合,再对有依据的峰值、典型表和耗时问题做有限补查。
12
+ - 用[固定 JSON 和 HTML 报告](topics/enterprise-report.md)交付画像、峰值来源、任务执行、关注表与耗时问题。关联缺失时保留已取得的统计,并说明覆盖。
13
+
8
14
  ## 解释更新记录
9
15
 
16
+ - 「分析 corp-xxx 租户近 3 天的更新情况」这类分析请求在对话中交付。需要窗口聚合时复用[企业更新负载分析](topics/enterprise-analysis.md)的相关取证方法,按用户的问题决定补查深度。
10
17
  - 先定下要解释哪些记录、用什么计数单位。request、chain、distinct node、attempt、load、engine task 要分开数,见[计数单位与身份](concepts.md#计数单位与身份)。手上有精确的 trace、chain、task 就直接用,不用重新找。
11
18
  - 只有企业 ID 或 dagId、还没找到具体执行时,按[从企业或 DAG 定位更新执行](topics/discover-executions.md)找候选。企业命中消费 span 之后,顺着已有的显式链接去查关联的更新。
12
19
  - 看不懂字段、阶段或关联时,按 [README](README.md) 选槽位,只读用得上的部分。这次记录能证明什么、不能证明什么见[能下什么结论](conclusions.md):每条关联边都要有它声明的原生关系或显式键,缺一条就停在最后证实的边界;同租户、同表名、时间挨着,只能用来筛候选。
@@ -0,0 +1,178 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
7
+ <title>企业更新负载报告</title>
8
+ <style>
9
+ :root{color-scheme:light;--bg:#f2f5f7;--ink:#152a35;--muted:#50656f;--line:#dbe6ea;--green:#007566;--tint:#eaf5f1;font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif}
10
+ *{box-sizing:border-box}
11
+ body{margin:0;background:var(--bg);color:var(--ink);font-size:14px;line-height:1.55}
12
+ main{max-width:1240px;margin:auto;padding:34px 28px 52px}
13
+ h1,h2,h3,p{margin:0}
14
+ h1{font-size:clamp(28px,4vw,46px);letter-spacing:-.04em;line-height:1.2;margin:8px 0 10px}
15
+ h2{font-size:23px;letter-spacing:-.025em}h3{font-size:16px}
16
+ small,.note{font-size:12px;color:var(--muted)}
17
+ #scope{color:var(--muted);font-weight:650}
18
+ #summary{font-size:16px;max-width:940px}
19
+ nav,.controls,.legend{display:flex;align-items:center;gap:8px 12px;flex-wrap:wrap}
20
+ nav{margin:13px 0}nav a{font-size:13px;color:#28515d;text-underline-offset:3px}
21
+ .kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:18px 0 12px}
22
+ .kpi,.panel{background:#fff;border:1px solid var(--line);border-radius:12px}
23
+ .kpi{padding:12px 14px}.kpi small,.kpi b{display:block}.kpi b{font-size:clamp(18px,3vw,28px);font-variant-numeric:tabular-nums}
24
+ .panel{padding:17px 19px;margin-top:12px;scroll-margin-top:12px}
25
+ .heading{display:flex;justify-content:space-between;align-items:start;gap:8px 16px;flex-wrap:wrap;margin-bottom:10px}
26
+ button,select{font:inherit;font-size:13px;color:var(--ink);background:#fff;border:1px solid var(--line);border-radius:7px;padding:5px 8px}
27
+ button,select,summary{cursor:pointer}
28
+ button:focus-visible,select:focus-visible,a:focus-visible,summary:focus-visible,.bar:focus-visible{outline:2px solid var(--green);outline-offset:2px}
29
+ [hidden]{display:none!important}
30
+ .legend{color:var(--muted);font-size:12px;margin-bottom:8px}
31
+ .legend i{display:inline-block;width:8px;height:8px;margin-right:5px;border-radius:2px}
32
+ .chart-scroll,.table-wrap{overflow-x:auto}
33
+ .chart-grid{display:grid;grid-template-columns:49px minmax(0,1fr);grid-template-rows:175px 23px;min-width:680px;padding:16px 0 0}
34
+ .y-axis{grid-column:1;grid-row:1;display:flex;flex-direction:column;justify-content:space-between;text-align:right;padding-right:6px;color:var(--muted);font-size:11px;line-height:1}
35
+ .plot{grid-column:2;grid-row:1;display:grid;border-bottom:1px solid #bacdd5;background:repeating-linear-gradient(to top,transparent 0,transparent calc(25% - 1px),#e9eff2 calc(25% - 1px),#e9eff2 25%)}
36
+ .hour-labels{grid-column:2;grid-row:2;display:grid;text-align:center;color:var(--muted);font-size:10px;padding-top:4px}
37
+ .hour-slot{position:relative;display:flex;align-items:end;justify-content:center;gap:1px;padding:0 3px;min-width:0}
38
+ .hour-slot.peak{background:#e7f4ef99}
39
+ .hour-labels .peak{color:#006658;font-weight:750}
40
+ .bar{width:100%;min-width:0;max-width:24px;padding:0;border:0;border-radius:2px 2px 0 0;background:var(--bar);height:var(--height);flex:1;position:relative}
41
+ .bar.partial{opacity:.64;background:repeating-linear-gradient(135deg,var(--bar) 0 3px,#fff 3px 4px)}
42
+ .bar.zero{height:2px}.bar.missing{height:2px;background:transparent;outline:1px dotted #9eb2ba}
43
+ .peak-value{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(var(--peak-height) + 2px);font-size:10px;font-weight:700;color:#005d50;white-space:nowrap;pointer-events:none}
44
+ .chart-readout{margin:6px 0;min-height:1.5em}
45
+ .chips{display:flex;flex-wrap:wrap;gap:5px 7px;align-items:center;margin-top:6px}
46
+ .chip{background:#edf3f6;color:#314b58;padding:2px 8px;border-radius:20px;font-size:12px}
47
+ .table-wrap{margin-top:8px}
48
+ table{width:100%;border-collapse:collapse;text-align:left;font-variant-numeric:tabular-nums}
49
+ th,td{padding:7px 9px;vertical-align:top;border-bottom:1px solid var(--line)}
50
+ th{color:var(--muted);background:#f3f7f8;white-space:nowrap;font-size:12px;font-weight:660}
51
+ td{font-size:13px}tr:last-child td{border-bottom:0}
52
+ .numeric{white-space:nowrap}
53
+ .table-id{display:block;color:var(--muted);font-size:11px;overflow-wrap:anywhere;margin-top:2px}
54
+ .peak-links a{color:#006254;text-underline-offset:3px}.source-detail{margin-top:8px;border-top:1px solid var(--line);padding-top:8px;scroll-margin-top:12px}
55
+ .source-row{display:grid;grid-template-columns:1fr 1.55fr;gap:14px;align-items:start}
56
+ .scene-distributions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:10px}
57
+ .scene-distribution{border:1px solid var(--line);border-radius:9px;padding:11px 13px}
58
+ .scene-pie-layout{display:flex;align-items:center;gap:14px;margin-top:10px;flex-wrap:wrap}
59
+ .scene-pie{width:124px;height:124px;border-radius:50%;flex:none}
60
+ .scene-pie-legend{list-style:none;margin:0;padding:0;flex:1;min-width:160px}
61
+ .scene-pie-legend li{display:grid;grid-template-columns:9px minmax(0,1fr) auto;gap:6px;align-items:center;font-size:12px;margin:3px 0}
62
+ .scene-pie-legend i{width:8px;height:8px;border-radius:2px}.scene-pie-legend code{overflow-wrap:anywhere}
63
+ .scene-pie-legend span{font-variant-numeric:tabular-nums;white-space:nowrap}.scene-total{margin-top:6px}
64
+ .readout{background:#f0f7f5;border:1px solid #dbeae6;border-radius:8px;padding:8px 10px;margin:9px 0}
65
+ .after{margin-top:15px}
66
+ .scope-table td:first-child{min-width:135px}
67
+ #attention-tables td:first-child{min-width:175px}
68
+ details summary{font-weight:650}
69
+ .timeline{min-width:160px;margin:6px 0;display:flex;height:8px;border-radius:4px;overflow:hidden;gap:2px}
70
+ .timeline span{background:#8d9aa1}.timeline .failed{background:#b34e35}.timeline .wait{background:#b2bdc4}.timeline .succeeded{background:#148073}
71
+ .timeline-labels{min-width:180px}
72
+ ul{padding-left:20px;margin:6px 0}
73
+ @media(max-width:740px){main{padding:20px 12px 38px}.panel{padding:13px}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.source-row,.scene-distributions{grid-template-columns:1fr}.chart-grid{grid-template-rows:143px 22px}}
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <main>
78
+ <header>
79
+ <p id="scope">__REPORT_SCOPE__</p>
80
+ <h1>企业更新负载报告</h1>
81
+ <p id="summary">__REPORT_SUMMARY__</p>
82
+ <nav aria-label="报告板块">
83
+ <a href="#overview">单日更新画像</a><a href="#origins">高峰更新来源</a><a href="#execution">任务执行情况</a><a href="#attention">关注表与耗时问题</a><a href="#definitions">指标口径</a>
84
+ </nav>
85
+ </header>
86
+ <div class="kpis" id="kpis" aria-label="窗口内指标">__REPORT_KPIS__</div>
87
+ <section id="overview" class="panel">
88
+ <div class="heading">
89
+ <div><h2>单日更新画像</h2><p class="note">同一小时并排比较。斜纹为部分覆盖,点线表示缺少该指标。</p></div>
90
+ __REPORT_CONTROLS__
91
+ </div>
92
+ <div id="chart" role="group" aria-label="按小时分组柱状图">__REPORT_CHART__</div>
93
+ <p id="chart-readout" class="note chart-readout" aria-live="polite">悬停柱子可查看小时数值。</p>
94
+ <div id="peak-links" class="controls peak-links">__REPORT_PEAK_LINKS__</div>
95
+ <div id="overview-scenes" class="chips"><span class="note">全窗更新链场景</span>__REPORT_SCENES__</div>
96
+ </section>
97
+ <section id="origins" class="panel">
98
+ <div class="heading"><h2>高峰更新来源</h2></div>
99
+ __REPORT_SOURCES__
100
+ </section>
101
+ <section id="execution" class="panel">
102
+ <div class="heading"><h2>任务执行情况</h2></div>
103
+ __REPORT_EXECUTION_SUMMARY__
104
+ <div class="table-wrap">
105
+ <table><thead><tr><th>指标</th><th>范围</th><th>有效记录</th><th>P50</th><th>P95</th><th>P99</th></tr></thead><tbody id="distributions">__REPORT_DISTRIBUTIONS__</tbody></table>
106
+ </div>
107
+ <h3 class="after">抽查的 Polars 执行</h3>
108
+ <div class="table-wrap">
109
+ <table><thead><tr><th>分析表与执行时间</th><th>任务做了什么</th><th>扫描行数</th><th>写出行数</th><th>分析表结果行数</th><th>表更新业务耗时</th></tr></thead><tbody id="samples">__REPORT_SAMPLES__</tbody></table>
110
+ </div>
111
+ </section>
112
+ <section id="attention" class="panel">
113
+ <div class="heading"><h2>关注表与耗时问题</h2></div>
114
+ <h3>值得关注的表</h3>
115
+ <div class="table-wrap">
116
+ <table><thead><tr><th>表名与 ID</th><th>场景与执行次数</th><th>最近可见结果</th><th>关注原因与证据</th><th>可继续核对</th></tr></thead><tbody id="attention-tables">__REPORT_TABLES__</tbody></table>
117
+ </div>
118
+ <h3 class="after">观察到的耗时问题</h3>
119
+ __REPORT_LATENCY_SUMMARY__
120
+ <div class="table-wrap">
121
+ <table><thead><tr><th>发生位置</th><th>时段与范围</th><th>问题</th><th>证据</th><th>结论范围</th></tr></thead><tbody id="latency">__REPORT_LATENCY__</tbody></table>
122
+ </div>
123
+ </section>
124
+ <section id="definitions" class="panel">
125
+ <div class="heading"><h2>指标口径与数据来源</h2></div>
126
+ <ul id="coverage" class="note">__REPORT_COVERAGE__</ul>
127
+ <div class="table-wrap scope-table">
128
+ <table><thead><tr><th>报告指标</th><th>数据来源</th><th>统计方式</th></tr></thead><tbody>
129
+ <tr><td>观测到的更新链</td><td><code>dispatch_submit</code> 的 <code>dagId</code>、<code>updateScene</code></td><td>在可归属的提交中按链 ID 去重。场景保留原始值。</td></tr>
130
+ <tr><td>表更新执行次数</td><td><code>dispatch_node_task_exec</code></td><td>一个表更新节点进入业务执行计一次。按场景分组用外层 <code>update.updateType</code>,覆盖可能与执行总数不同。</td></tr>
131
+ <tr><td>主队列消费次数</td><td>主队列的 <code>node-task-queue receive</code></td><td>主队列每处理一条投递计一次。同一明细再次消费会再计。</td></tr>
132
+ <tr><td>调度侧转入 slow node queue</td><td><code>Tenant limit exceeded</code>、<code>receive executeInfo at slow queue</code></td><td>按租户统计并发额度不足时的转发,按执行明细核对慢队列消费和回主队列。慢队列消费次数单独取证。</td></tr>
133
+ <tr><td>引擎资源组排队</td><td>同一次更新的引擎任务与日志,如 <code>ResourceGroupQueueTimeoutException</code></td><td>按该次任务阶段耗时或同次错误日志确认,逐次记录重试和等待。</td></tr>
134
+ <tr><td>写入任务接收行数与输出量</td><td><code>write.metric.received</code>、<code>write.metric.output_data_mb</code></td><td>汇总已解析、未跳过且接收行数为正的写入。输出量按有效上报的 MiB 换算。实际落盘量另看对应字段的覆盖。</td></tr>
135
+ <tr><td>表更新业务耗时与结果行数</td><td><code>update.businessConsumed</code>、<code>update.rowCount</code></td><td>耗时分位数只使用有值的记录。结果行数取对应更新时点。</td></tr>
136
+ <tr><td>Polars 扫描与写出行数</td><td>同一次任务的 <code>scanned_rows_num</code>、<code>writtenRows</code></td><td>记录抽样任务读入和写出的规模。</td></tr>
137
+ </tbody></table>
138
+ </div>
139
+ </section>
140
+ </main>
141
+ <script type="application/json" id="report-data">__REPORT_DATA__</script>
142
+ <script>
143
+ "use strict";
144
+ const controls = document.getElementById("chart-controls");
145
+ if (controls) {
146
+ const day = document.getElementById("day");
147
+ const measure = document.getElementById("measure");
148
+ const readout = document.getElementById("chart-readout");
149
+ const charts = document.querySelectorAll("#chart [data-metric]");
150
+ const dated = document.querySelectorAll("#chart [data-day]");
151
+ function selectChart() {
152
+ for (const chart of charts) chart.hidden = chart.dataset.metric !== measure.value;
153
+ for (const item of dated) {
154
+ item.hidden = item.classList.contains("peak-value")
155
+ ? item.dataset.day !== day.value
156
+ : Boolean(day.value && item.dataset.day !== day.value);
157
+ }
158
+ readout.textContent = "悬停或聚焦柱子查看这一小时的数值。";
159
+ }
160
+ for (const bar of document.querySelectorAll("#chart .bar[title]")) {
161
+ const announce = () => { readout.textContent = bar.title; };
162
+ bar.addEventListener("focus", announce);
163
+ bar.addEventListener("pointerenter", announce);
164
+ }
165
+ day.addEventListener("change", selectChart);
166
+ measure.addEventListener("change", selectChart);
167
+ selectChart();
168
+ controls.hidden = false;
169
+ }
170
+ for (const link of document.querySelectorAll('a[href^="#peak-"]')) {
171
+ link.addEventListener("click", () => {
172
+ const details = document.getElementById(link.hash.slice(1));
173
+ if (details) details.open = true;
174
+ });
175
+ }
176
+ </script>
177
+ </body>
178
+ </html>