报表统计:数据窗口、分片计算与结果发布
日报由明确的数据窗口产生。用户登录时可以查看报表生成状态或请求补生成,但不应让首位登录用户承担全天所有报表的计算。
示例中的所有租户统一使用 Asia/Shanghai 时区:每天 02:00 开始生成前一天的销售报表,08:00 前交付。任务参数固定保存租户、起止时间、统计口径版本和本轮数据水位。
从查询页面推导离线发布
假设经营者只想临时看今天的订单总额,数据量小且查询开销可控时,在线聚合可能已经足够。需求变成“每天固定时间交付昨天的正式报表,并保留可复核版本”后,计算和发布就有了独立生命周期。
| 候选方案 | 合适的条件 | 需要承担的成本 |
|---|---|---|
| 用户打开时实时聚合 | 临时查询、允许每次结果变化 | 请求延迟与数据库压力 |
| 定时覆盖一份汇总结果 | 允许中间覆盖且不需版本追溯的内部统计 | 需要解释失败重算和结果可见时点 |
| 独立运行、分片暂存、完整发布 | 要求正式交付、可修订、可恢复 | 运行记录、分片校验与发布指针 |
本篇选择第三种。report_run 回答“这一轮算的是什么”,report_shards 回答“哪些范围已完成”,正式指针回答“用户现在应该看到哪一版”。拆分由交付和恢复要求产生,不是为了增加表的数量。
计算与发布的执行时序
分片计算完成只说明中间结果已保存,用户可见的版本要到发布事务成功后才改变。
分片可并发处理,图中循环表示每片都要独立确认。重复成功消息不能改变预期分片集合,也不能让缺失分片自动算作完成。
报表需求与交付结果
示例要求每个租户在上海时间每天 08:00 前看到完整的前一日报表。02:00 是计划开始时间,08:00 才是交付期限;业务是否允许展示旧版本、晚到数据怎样修订,都应先约定。
| 需求 | 示例决策 | 开发约束 |
|---|---|---|
| 展示什么 | 最后一次完整发布的正式版本 | 用户查询不直接读取中间汇总 |
| 没有交易怎么办 | 发布零数据报表并标明数据范围 | 不把空结果当成任务没执行 |
| 允许谁重跑 | 有权限的租户管理员或内部操作员 | 记录原因、版本和操作者 |
| 迟到数据怎么办 | 建立修订版本,保留原版 | 发布指针条件更新 |
| 交付延迟怎么办 | 展示生成状态并通知负责人 | 成功通知只在发布后产生 |
时间窗口和业务口径
| 决策 | 示例 | 不明确时的后果 |
|---|---|---|
| 时间字段 | 支付成功时间 | 下单和支付统计混淆 |
| 窗口边界 | 上海时间昨日 00:00 到今日 00:00,左闭右开 | 边界订单重复或遗漏 |
| 数据截止 | 02:00 生成时允许使用到哪个水位 | 多次读取出现不同结果 |
| 金额规则 | 实收、退款、手续费分别统计 | 各报表无法核对 |
| 修订规则 | 迟到数据触发新版本 | 旧报表被无声覆盖 |
重试沿用原窗口,不重新计算“昨天”。历史重跑需要显式的日期与口径,避免恢复任务在今天处理错误窗口。
若产品按租户当地日期统计,应按每个租户的 IANA 时区分别确定窗口、计划时间与交付期限,再转换为统一时间戳调度。不能把所有租户固定在上海时间 02:00 开始,并假定它们的前一日本地窗口都已结束。
数据结构与接口
| 记录 | 关键字段与约束 |
|---|---|
report_definition | 口径版本、时区、窗口规则、输出格式、授权范围 |
report_run | 租户、窗口、口径、数据水位、修订号、状态;同一运行身份唯一 |
report_shards | 运行 ID、分片 ID、输入范围、结果摘要;运行内分片唯一 |
report_artifacts | 运行 ID、产物 URI、校验值和有效期 |
report_publications | 报表逻辑身份、当前正式运行 ID、发布代数 |
创建重跑请求时返回运行 ID,查询接口返回等待、计算、校验、发布或失败阶段。下载接口按租户和报表权限授权,不因文件地址存在就允许访问。
手动重跑与自动调度使用相同的运行创建逻辑;同一请求键不能重复创建修订。正式发布指针可以独立于运行记录,避免多个修订争抢一个布尔 published 字段。
怎样触发与保存进度
日历调度器生成本轮运行记录,逻辑报表身份可采用 tenant_id + window_start + metric_version,运行身份再加入修订号。定时初版使用约定的初始修订,人工重算按请求键分配新修订。运行记录包含待计算分片、完成数、失败数和当前发布版本。
同一轮重复调度命中已有记录。若要修订数据,建立新的运行版本,并说明使用了哪个数据水位;不要通过更换随机任务 ID 意外生成两份互相竞争的正式报表。
任务创建与分片计算
调度器或重跑 API:
确定窗口、口径、水位与修订号
事务创建 report_run、预期分片清单与计算任务
分片 Worker:
领取任务,确认运行未取消且输入版本仍有效
在事务外读取固定范围,生成本次暂存结果
事务校验领取资格,写入本分片结果和校验摘要
确认计算任务,不通过“累计加一”重复统计完成数
汇总 Worker:
核对预期分片身份与完整性
合并为本轮产物,再进入发布事务完成数量应从唯一分片结果或受唯一约束保护的完成事件计算。一个分片的消息重复到达,不能让运行提前达到“全部完成”。
1000 个租户的日报
调度器分批枚举 1000 个租户,为每个租户独立创建 report_run 和所属分片任务。这里的调度批次只用于组织枚举,不是跨租户共用的一条 report_run。每个租户分别计算进度、失败与发布结果,单个大租户还可以按时间段或业务分区拆开,但每个分片必须覆盖互不重叠的数据范围。
Worker 并发受查询资源限制。把 1000 个 SQL 同时提交给在线数据库,会让报表和业务请求争用连接、CPU 与磁盘。先测单任务开销,再限制总并发;必要时使用允许延迟的副本或专用分析存储。
如果使用只读副本,要记录复制延迟和业务水位。副本落后时不能把暂时没同步的订单当成最终不存在。
分片读取的一致性
各分片使用同一个“统计日期”并不意味着读取了同一份数据。任务运行期间仍可能有退款和补录。
可以按业务水位、不可变流水或明确的数据快照读取。PostgreSQL 的 Repeatable Read 可以为单个事务提供稳定快照,但不同 Worker 的独立事务并不会自动共享同一快照,参见事务隔离。
跨 Worker 的一致快照需要额外协调;长时间维持事务也会增加数据库维护成本。没有这项设计时,应明确报表允许多大迟到与修订范围。
完整后再发布
创建本轮 report_run
按租户或数据分区生成计算任务
各任务把结果写入本轮暂存区域,按分片键幂等覆盖
汇总器核对预期分片是否全部成功
校验金额、行数与数据水位
原子切换正式版本,保存“报表可用”通知意图分片重试只替换自己的结果,不能再次累加到已经完成的汇总值。用户访问到的是最后一个完整发布版本;部分失败的新版本显示生成中或失败,不伪装成完整报表。
防止旧修订覆盖新发布
产物在事务外生成,正式指针在短事务中切换。下面的设计片段假定首次发布槽已创建,示例以预期发布代数防止并发覆盖:
UPDATE report_publications
SET current_run_id = :validated_run_id,
publication_version = publication_version + 1
WHERE tenant_id = :tenant_id
AND report_key = :report_key
AND publication_version = :expected_publication_version
RETURNING current_run_id, publication_version;事务内还要锁定并校验运行已完整通过检查,保存发布结果与通知意图。竞争失败后重新读取当前修订,依据修订顺序决定是否跳过;不能无条件覆盖刚发布的新版本。
publication_version 只是对发布槽的并发检查,不能单独判断内容新旧。validated_run_id 还必须属于相同租户、窗口与口径,并通过输入水位和修订顺序检查;否则一条 SQL 成功更新也可能发布错误报表。
尚未发布的产物失败后可以重建,已经发布的下载结果保持不变。产物回收要避开正式指针和仍有效的下载引用。
通知和补跑
1000 个租户都生成完成时,可以各自生成一条可追踪的通知意图,再按渠道限速发送。频繁重算同一份报表不应重复提醒全部订阅人;发布版本与通知策略需要有独立业务键。
停机后按缺失窗口补跑,先核对业务所需的最晚交付时间。对跨多天的历史补算,保留处理水位和分片进度,控制其资源份额,给当前日报留出容量。
一次分片重试与发布竞争
假设租户 T8 的日报运行 R30 有 10 个分片,当前正式版本还是 R29:
| 操作 | 中间记录 | 用户看到什么 |
|---|---|---|
| R30 的 9 个分片完成,第 10 个失败 | 9 份唯一结果,第 10 份待重试 | 仍是完整 R29,R30 显示生成中 |
| 第 3 个分片重复报告成功 | 仍是同一个 shard_id 的结果 | 完成数不能因此变成 10 |
| 第 10 个恢复,全部校验通过 | R30 进入可发布状态 | 尚未切换前仍是 R29 |
| 更高修订 R31 抢先发布 | 指针与发布代数已推进 | 完整 R31 |
| R30 再尝试发布 | 预期代数冲突,重新核对修订后退出 | R31 不被旧结果覆盖 |
由此可以检查一段实现是否只“看起来能重试”:如果重复处理通过总金额再次相加,或只靠完成计数器加一,就无法得到上表结果。可恢复的结果应按稳定分片身份覆盖或确认。
验收与排查依据
| 场景 | 应断言的结果 |
|---|---|
| 同一轮重复调度 | 运行身份相同,不重复展开分片 |
| 一个分片失败,其余成功 | 不发布半份结果,只重试失败分片 |
| 分片成功但确认响应丢失 | 重试覆盖同一分片,不重复累加金额 |
| 老修订晚于新修订完成 | 老结果不能覆盖当前正式指针 |
| 数据源暂时为空或副本落后 | 区分真实零数据、数据未到齐和执行失败 |
| 租户已停用或权限撤销 | 停止未授权生成或下载,记录处理原因 |
记录窗口、水位、预期分片集合、校验结果、发布修订与截止时间。告警同时观察交付延迟和最老未完成分片,不能只看计算进程是否存活。
SchedulerX 跑批模型与报表发布条件
阿里云开发者社区中,千习的 SchedulerX 场景介绍列举了周期报表生成与推送,以及数据量增长后使用分布式批处理的思路。这是一篇产品方案介绍,不能据此认定某家客户采用了本文的报表模型。其实现约束可进一步对照官方的 MapReduce 模型文档。
官方文档说明,故障转移可能重复执行子任务,需要业务幂等;同时 runReduceIfFail 的默认行为允许子任务失败后仍执行 reduce。汇总回调被调用,因而不能作为“全部分片成功”的证明。接入时应核对所用客户端版本及配置,并在汇总器中检查预期分片、实际状态和结果完整性,再允许正式发布。
本文建议将大结果写入本轮暂存区,子任务只返回结果位置与校验摘要。官方还提示 reduce 收集结果会对 Master 内存形成压力,因此不能让 1000 个租户把整份报表正文都返回主节点。调度平台承担任务分发,数据水位、修订顺序和正式版本切换仍需由报表业务实现。
火山引擎 FlowX:依赖满足、时间到达和资源可用
火山引擎数智平台 VeDI 的 FlowX 工作流调度实践将任务定义与按业务日期生成的实例分开,并以留存率计算说明:一个实例可能同时依赖当天和数日前的上游实例。其依赖检查、时间检查及配额管理也分别承担不同职责。DataLeap 调度依赖文档则明确要求上游无失败、到达执行时间且调度资源充足。
对日报而言,“凌晨两点执行”还应落实为下面的具体条件;以下为本文设计示例:
| 条件 | 开发实现 | 未满足时的状态 |
|---|---|---|
| 对应业务日的订单与退款输入齐备 | 保存输入分区、业务日期和就绪证据 | 等待数据 |
| 到达允许生成时间 | 使用明确时区的计划时间 | 等待时间 |
| 获得报表资源额度 | 为日常运行和历史重算分别设并发预算 | 等待资源 |
| 分片完整且校验通过 | 核对本轮运行的结果清单 | 等待修复或允许发布 |
FlowX 文章还介绍了通过上游成功事件重新触发依赖检查、通过探针检查外部数据,以及按时间区间配置配额。可据此将 1000 个租户的日常日报与历史补算隔离,避免补一个月历史数据占满当天交付所需的资源。任务定义之间存在依赖,不等于不同业务日期的实例可以混用数据。
阿里云重刷实践:运行时间与数据时间分开
SchedulerX 的重刷数据说明展示了今天触发一组历史日期实例的方式,并注明该能力支持的调度周期范围。它提供了区分当前运行时间和历史数据时间的具体产品例子。
本文的补跑接口应显式传入业务日期、规则版本与重跑原因,不在处理器里用“当前时间减一天”决定报表日期。重算旧日期时写入新的运行修订,验证后再切换该日期的正式指针;是否重新通知用户另作决策。同一日期允许多次尝试,但每次尝试不能把历史金额再次追加到已发布总额。
参考资料
- 火山引擎数智平台 VeDI:DataLeap 背后的工作流编排调度系统 FlowX
- 火山引擎官方文档:DataLeap 任务调度依赖
- 阿里云官方实践:SchedulerX 如何重刷数据
- 千习,阿里云开发者社区:SchedulerX 如何帮助用户解决分布式任务调度难题?
- 阿里云官方文档:SchedulerX MapReduce 模型
- PostgreSQL 18:Transaction Isolation
- PostgreSQL 18:UPDATE:条件更新与 RETURNING