简介:clusterdata 汇集阿里巴巴多版本生产集群追踪数据,面向数据中心、集群调度与负载建模方向的学习者和科研人员,帮助理解现代互联网 IDC 的机器规模、在线服务与批处理工作负载特征,也适合用于集群管理相关课程项目与论文实验。压缩包共 32 个文件,约 16.22MB,以说明文档、结构表、分析脚本和示例笔记为主,辅以图示、头文件定义、许可证与校验文件;这些材料分别承担字段查阅、分析复现、结果示意和数据完整性核对等多种用途。内容覆盖 2017、2018 及 2020 等追踪版本,并配有结构定义、说明文档与架构图示,可支撑负载特征统计、资源调度、在线与批处理任务搭配和有向无环图关系分析。已有 1162 人学习下载,适合需要真实生产 trace 做实验复现、课程设计或研究对照的中高级读者按模块查阅、对照与二次分析。
1. 生产集群的负载数据到底能回答哪些问题
机房里最尴尬的一幕,是容量评审会上有人问「集群到底忙不忙」,全场沉默——大盘上 CPU 均值 18%,业务侧却天天喊排队。clusterdata 这类从生产集群直接采集下来的负载数据,就是为这种场面准备的:谁在什么时候提交了什么任务、要了多少核多少内存、实际用了多少、跑了多久、最后成没成、落在哪台机器上。它把调度器事件、节点采集时序、任务状态机三处原始记录拼成一条可复盘的时间线,让你能在自己的笔记本上重演一整天的集群行为。
它解决的不是「看一眼监控」的需求,而是三类必须动数据的问题:做调度算法要真实负载回放来验证策略、做容量规划要摸清长尾和碎片、做异常检测缺真实的正负样本。适合调度、SRE、容量、算法四个方向上真正要出结论的人。下面按采集口径、清洗、回放仿真、避坑的顺序,把我踩过的路讲一遍。
2. 数据是怎么被采集出来的:三个源头、两套时钟、一堆空洞
拿到一份生产集群数据,最容易犯的错是先写groupby再看字段说明。生产数据的形状是采集链路决定的,不了解链路,你算出来的每一个均值都可能是假的。这一章先把采集侧讲透,再落到怎么验证。
2.1 三个采集源的时间语义并不一致
生产集群里,一份可用的负载数据集通常由三张表拼出来,它们分别来自不同组件,精度和口径完全不同。
| 采集源 | 记录内容 | 时间精度 | 典型问题 |
|---|---|---|---|
| 调度器事件日志 | 提交、入队、调度成功、调度失败、抢占、驱逐 | 毫秒,事件驱动 | 只有状态变化,没有实际用量;失败重试会重复出现 |
| 节点采集时序 | 单机或单容器的 CPU、内存、磁盘、网络水位 | 固定间隔,常见 15s 到 60s | 掉线、补点、时钟漂移、把整机口径当容器口径 |
| 任务状态机 | 任务最终状态、实际资源用量、退出码 | 秒级,终态上报 | 长任务延迟上报,短任务可能根本没有记录 |
关键在于「提交时间」由用户侧网关写入,「开始运行时间」由调度器写入,「结束时间」由节点上报后回写,三处时钟不同源。我做排队时长分析时,第一版直接算start_ts - submit_ts,结果出现了一批负值,占比千分之三左右。这个量级不大,但它说明两件事:一是时钟偏差真实存在,二是如果你拿这批数据去训练一个预测排队时长的模型,负样本会把损失函数带偏。
常见做法是给每个数据源单独估算偏移:取同一批任务在两张表里的时间戳差值,做中位数估计,超过中位数 3 倍绝对偏差的记录标记为可疑但保留,超过 60 秒的直接剔除。不要去「修正」时间戳,只打标记,因为偏移量本身可能是负载相关的,修正反而引入新偏差。
2.2 采样间隔决定了你能看到多细的长尾
节点采集是定时的,这意味着一切短于采样间隔的行为都在数据里消失了。假设采集间隔是 60 秒,一个跑了 8 秒的任务,在时序表里可能只有一条记录、可能一条都没有。如果你用「有记录的任务」去做时长分布,会得到一条被截断的曲线,短任务那一端被系统性砍掉。
更麻烦的是均值抹峰值。一台机器在 60 秒内 CPU 冲到 100% 持续 10 秒、剩下 50 秒在 20%,均值记录可能是 33%。你拿这个 33% 去做容量规划,结论是「还有余量」,而实际的 SLO 早就被打穿了。处理办法有三个层次:
第一,凡是能拿到任务级实际用量的,优先用任务级数据聚合,别用机器级时序。任务级数据是按生命周期记的,天然不受采样间隔影响。
第二,必须用机器级时序时,做分位数而不是均值,并且明确报告口径:P99 的 P99、还是逐分钟最大值再取 P99。这两种口径在同一份数据上能差出 15 个百分点。
第三,短任务单独统计。把时长小于一个采样间隔的任务拎出来,只统计它们的请求值、到达规律和失败率,不参与利用率计算。
缺失值也要分清三种形态,它们的处理方式完全不同:
NaN:真正没有记录,通常是 agent 掉线或上报失败,可以插值也可以丢弃,取决于你要看的时间尺度。0:agent 正常上报但值为 0,可能是容器刚创建还没开始干活,也可能是写错了。连续多个 0 才可疑。- 断点:整段时间序列里根本没有这台机器,但在任务表里它有任务。这类最危险,会被误判成「空闲机器」拉低整体利用率。
下面这段代码就是用来把这三类问题先摸清楚的,跑完再决定要不要往下做。
import pandas as pd # 机器资源时序:一行 = 某台机器在某个采集点的水位 # 列约定: machine_id, ts(秒级时间戳), cpu_usage, mem_usage ts = pd.read_parquet("node_metric/day=20240101/") ts["ts"] = pd.to_datetime(ts["ts"], unit="s", utc=True) # 1) 单机采样间隔分布:正常应集中在一个固定值,长尾说明掉线或补点 gap = (ts.sort_values(["machine_id", "ts"]) .groupby("machine_id")["ts"] .diff() .dt.total_seconds()) print(gap.value_counts().head(10)) print("P99 间隔(秒):", gap.quantile(0.99)) # 2) 覆盖时长:低于中位数 80% 的机器先踢出画像,否则会稀释利用率 cover = ts.groupby("machine_id")["ts"].agg(["min", "max", "count"]) cover["span_h"] = (cover["max"] - cover["min"]).dt.total_seconds() / 3600 bad = cover[cover["span_h"] < 0.8 * cover["span_h"].median()] print("疑似掉线机器数:", len(bad)) # 3) 区分真缺失和 0:连续 3 个点以上为 0 才算可疑空闲 ts = ts.sort_values(["machine_id", "ts"]) zero_run = (ts.groupby("machine_id")["cpu_usage"] .transform(lambda s: s.eq(0).rolling(3, min_periods=1).sum())) print("连续 3 点全 0 的记录占比:", (zero_run >= 3).mean())逻辑说明:第一段看间隔分布,value_counts的头部应该是单峰,如果出现 120s、180s 这种整数倍,说明存在补点;第二段按机器算覆盖时长,span_h是个粗筛,能把整段时间没数据的机器捞出来;第三段用滚动窗口区分「一个点 0」和「连续 0」,前者大概率是容器冷启动,后者才是真异常。
参数上,0.8这个系数是经验值,机器规模大的时候可以放宽到 0.7,但不要低于 0.5,否则掉线机器会污染画像。滚动窗口长度取 3,是因为 3 个间隔(默认 60s)已经覆盖了绝大多数冷启动时间,再长会把真实的空闲机器误杀。
2.3 把任务表和机器表对齐成一条事件流
两张表对齐的键通常不是任务 ID,而是「实例 ID + 时间窗」。同一台机器可能被重装过、IP 复用、机器 ID 复用,你按machine_id直接 join,会把两个不同时期的状态串到一起。
对齐的实操顺序是:先以任务表为主表,每条任务带上machine_id和[start_ts, end_ts]区间;再把机器时序按machine_id分组,用区间切片取窗口内的采集点;最后把「窗口内采集点数 / 区间长度应有点数」算成一个覆盖率字段。覆盖率低于 0.6 的任务,标记为「时序不可信」,只保留请求值参与后续分析。
注意:覆盖率这个字段一定要保留下来并写进结论。我见过一份调度优化报告,收益里有一半来自「降低了平均等待时间」,后来发现那批任务恰好是覆盖率最低、时长被截断得最厉害的一批。
还有一层容易忽略的粒度问题:作业和任务是两回事。一个作业提交后可能拆成几十个任务(实例),调度器是按任务排队、按作业算成功率的。你做到达率分析用任务粒度,做失败归因用作业粒度,混用会让到达率虚高一个数量级。常见做法是在任务表上保留job_id,聚合前明确一句「下文所有到达率均指任务级」。
3. 把数据落到本地:解析、清洗与第一轮画像
数据落地的目标不是「读进来」,是「得到一组你能拿出去跟人争论的数字」。这一章给一套可以照抄的流程,跑完你手上会有一张集群画像表,后面所有分析都从它出发。
3.1 按天分区读入,只取需要的列
生产集群数据一天的量级往往超出单机内存的舒适区,直接把整个目录读进 pandas 是新手最容易翻的车。两个动作可以省掉大部分麻烦:按分区裁剪、按列投影。
常见做法是用对象存储的目录约定(day=YYYYMMDD/hour=HH)做分区,读的时候只读需要的分区和列。pandas 用columns=参数,polars 用select,效果一样。如果你的数据是压缩的 CSV,一列一列读比整行读快得多,因为它可以跳过没用的列。
import polars as pl # 只读一天、只读 8 个字段,内存占用通常能降到全量读取的 1/5 task = pl.scan_parquet("task_events/day=20240101/*.parquet").select([ "task_id", "job_id", "machine_id", "submit_ts", "start_ts", "end_ts", "req_cpu", "req_mem", "status", ]).collect() print(task.shape) print(task.select(pl.col("status").value_counts()))逻辑说明:scan_parquet是惰性的,select会在扫描阶段就下推,所以它不会先把整个文件解出来再裁列。这一步省下来的不是几秒钟,是几十分钟。
字段名在不同数据集里可能有细微差别,比如req_cpu也可能叫cpu_request、plan_cpu。不要凭记忆写死,先把 schema 打出来对一遍,成本 10 秒,收益是省掉一次两小时的调试。
print(pl.scan_parquet("task_events/day=20240101/*.parquet").collect_schema())3.2 四步清洗:去重、对时、截断、补时长
清洗不是越干净越好。生产数据的「脏」往往承载着真实信息,比如同一个task_id出现多次通常意味着重试,这不是噪声,是失败信号。所以清洗的原则是:只做能解释的动作,每一步留下可追溯的标记列。
四步按顺序做:
第一步,去重。同一个task_id有多条记录时,按end_ts取最后一条,同时把记录条数写成retry_cnt字段保留下来。这样既保证一行一任务,又没丢掉重试信息。
第二步,对时。三个时间戳统一成 UTC 秒,然后算start_ts - submit_ts(排队时长)和end_ts - start_ts(运行时长)。把排队时长为负的记录标记出来,不删。
第三步,截断。资源请求值出现超过单机规格的,几乎一定是脏数据或者是「虚拟超卖单位」,两者处理方式不同。稳妥做法是按机型规格截断到上限并标记,而不是直接删,因为这类记录往往集中在某个业务线,删了会系统性低估那个业务线的规模。
第四步,补时长。end_ts缺失的任务,多数是长任务跨了数据采集窗口。用数据窗口的截止时间填,并打一个censored=True标记。做时长分布时这批要单独算,否则会低估长尾。
import numpy as np import pandas as pd # 列约定: task_id, job_id, machine_id, submit_ts, start_ts, end_ts, # req_cpu, req_mem, status task = pd.read_parquet("task_events/day=20240101/") # 1) 去重但保留重试次数 task["retry_cnt"] = task.groupby("task_id")["task_id"].transform("size") task = task.sort_values("end_ts").drop_duplicates("task_id", keep="last") # 2) 对时:统一 UTC,负排队时长只标记不删 for c in ["submit_ts", "start_ts", "end_ts"]: task[c] = pd.to_datetime(task[c], unit="s", utc=True, errors="coerce") task["queue_s"] = (task["start_ts"] - task["submit_ts"]).dt.total_seconds() task["neg_queue"] = task["queue_s"] < 0 print("负排队占比:", task["neg_queue"].mean().round(5)) # 3) 截断:按机型上限 96 核 / 512 GiB 截断并打标 CPU_CAP, MEM_CAP = 96, 512 task["req_cpu_clip"] = task["req_cpu"].clip(upper=CPU_CAP) task["req_mem_clip"] = task["req_mem"].clip(upper=MEM_CAP) task["clipped"] = (task["req_cpu"] > CPU_CAP) | (task["req_mem"] > MEM_CAP) # 4) 补时长:跨窗口的长任务用窗口截止时间填,单独标记 WINDOW_END = pd.Timestamp("2024-01-02", tz="UTC") task["censored"] = task["end_ts"].isna() task["end_ts"] = task["end_ts"].fillna(WINDOW_END) task["dur_s"] = (task["end_ts"] - task["start_ts"]).dt.total_seconds() print(task[["queue_s", "dur_s"]].describe(percentiles=[0.5, 0.9, 0.99]))逻辑说明:transform("size")在去重前算,才能拿到原始的重试次数;clip而不是drop,是为了保留业务线的规模信号;censored标记必须留着,后面算 P99 时长时要把它排除或者做生存分析校正。
参数上,CPU_CAP和MEM_CAP一定要按你所用集群的真实机型改,写死一个通用值会让截断逻辑失真。如果你拿不到机型信息,退一步的做法是先画请求值的分布直方图,找那个「突然掉到 0」的位置当上限。
3.3 十行代码出一张集群画像表
清洗完的第一件事不是建模,是出一张画像表。这张表要能回答四个问题:集群有多忙、任务有多长、请求和实际差多少、失败率在什么水平。
| 指标 | 计算口径 | 用来判断什么 |
|---|---|---|
| 时间加权利用率 | 各机器 CPU 时序在窗口内的均值,再按容量加权 | 集群整体是否有余量 |
| 请求/实际比 | sum(req_cpu * dur) / sum(usage_cpu * dur) | 超卖空间有多大 |
| 时长 P50 / P90 / P99 | 按dur_s分位数,排除censored | 长尾有多长,是否影响调度 |
| 短任务占比 | dur_s < 采样间隔的任务数占比 | 时序数据可信度 |
| 失败率 | status == failed占任务数,按业务线拆 | 是否有异常业务线污染 |
| 排队 P99 | queue_s的 99 分位 | 调度器是否成为瓶颈 |
prof = { "util": (ts["cpu_usage"] * ts["cpu_cap"]).sum() / ts["cpu_cap"].sum(), "req_over_use": (task["req_cpu_clip"] * task["dur_s"]).sum() / (task["used_cpu"] * task["dur_s"]).sum(), "dur_p99_h": task.loc[~task["censored"], "dur_s"].quantile(0.99) / 3600, "short_ratio": (task["dur_s"] < 60).mean(), "fail_rate": (task["status"] == "failed").mean(), "queue_p99_min": task["queue_s"].quantile(0.99) / 60, } print(prof)这张表最大的价值是「对齐认知」。同一个集群,容量团队看利用率、调度团队看排队 P99、业务团队看失败率,三拨人能得出三个相反的结论。把六个数放在一张表上,争论才有共同的起点。
4. 负载回放与调度仿真:把一天的集群行为重演一遍
画像只能告诉你现状,要回答「换个策略会怎样」,必须做回放与仿真。这一章给一个最小闭环:从数据里抽出到达序列和资源请求分布,喂给一个两百行的离散事件仿真器,跑出策略对比。
4.1 从提交时间戳抽到达序列,先验证它是不是泊松
几乎所有调度论文都假设任务到达是泊松过程,而生产数据几乎从来不是。所以第一步不是生成泊松流,是验证。
做法很简单:按分钟聚合提交数,算均值和方差。泊松过程的方差等于均值,如果方差远大于均值,就是突发流量;如果远小于均值,说明被限流或者有批处理定时任务。
arr = task.set_index("submit_ts").resample("1min").size() print("每分钟到达 均值/方差:", round(arr.mean(), 2), round(arr.var(), 2)) print("突发系数(方差/均值):", round(arr.var() / arr.mean(), 2)) # 系数 ≈1 -> 可近似泊松;>>1 -> 突发,必须用真实序列回放逻辑说明:resample("1min")会把没任务的分钟补 0,这一点很重要,不补 0 会把方差算小。突发系数在 1 附近时可以用泊松生成器做扩展实验;到了 5 以上,任何用泊松流得出的调度结论都要打折,因为真实流量的尖峰才是把调度器打崩的原因。
我一般会再补一步:把到达序列按小时切段,看每段的突发系数。很多集群呈现出「白天平稳、整点尖峰」的形状,整点那几个批任务才是真正需要仿真的部分。只回放平稳段,等于把最难的题跳过去了。
4.2 一个两百行能跑起来的离散事件仿真
仿真器不用上框架,heapq加几个数组就够,关键是把三个语义写对:资源归还发生在任务结束时刻、调度决策发生在任务到达时刻、装不下时要记录而不是静默丢弃。
import heapq def simulate(tasks, num_nodes, node_cpu=96, node_mem=512, oversell=1.0): """最简调度仿真:FIFO 队列 + first-fit 装箱。 tasks: [(submit_ts, dur_s, cpu, mem), ...],时间单位统一为秒 oversell: 超卖比,1.0 表示不超卖 """ cap_cpu, cap_mem = node_cpu * oversell, node_mem * oversell free_cpu = [cap_cpu] * num_nodes free_mem = [cap_mem] * num_nodes release = [] # 小顶堆: (释放时刻, 节点下标, cpu, mem) placed, rejected = 0, 0 last_end = 0.0 for submit, dur, cpu, mem in sorted(tasks): # 1) 先把 submit 之前已释放的资源还回去(惰性归还) while release and release[0][0] <= submit: _, idx, c, m = heapq.heappop(release) free_cpu[idx] += c free_mem[idx] += m # 2) first-fit:找第一台装得下的机器 idx = next((i for i in range(num_nodes) if free_cpu[i] >= cpu and free_mem[i] >= mem), -1) if idx < 0: rejected += 1 # 装不下就记拒绝,不重排队,保持语义简单 continue free_cpu[idx] -= cpu free_mem[idx] -= mem end = submit + max(dur, 1.0) # 兜底 1 秒,避免零时长任务死循环 heapq.heappush(release, (end, idx, cpu, mem)) placed += 1 last_end = max(last_end, end) return { "placed": placed, "reject_rate": round(rejected / max(len(tasks), 1), 4), "makespan_h": round(last_end / 3600, 2), "avg_free_cpu": round(sum(free_cpu) / num_nodes, 2), } # 用法:从真实数据抽样,时间按 submit_ts 归零 base = task["submit_ts"].min() tasks = list(zip( (task["submit_ts"] - base).dt.total_seconds(), task["dur_s"], task["req_cpu_clip"], task["req_mem_clip"], )) print(simulate(tasks[:20000], num_nodes=500, oversell=1.0)) print(simulate(tasks[:20000], num_nodes=500, oversell=1.3))逻辑说明:惰性归还是关键设计——只在有任务到达时才弹出到期事件,避免为每个任务单独维护定时器,两万个任务跑下来不到一秒。first-fit是最朴素的装箱,换成 best-fit 只需要把next(...)换成一个min,这也是对比实验最方便的地方。
参数上,num_nodes必须由画像反推:用sum(req_cpu * dur) / (窗口时长 * 单机核数 * 目标利用率)估一个下界,再上下浮动 20% 做敏感性。oversell从 1.0 到 1.5 扫一遍,看拒绝率的拐点在哪——那个拐点就是超卖的安全边界。
提示:
rejected的语义是「此刻没有整块资源」,不是「任务失败」。做策略对比时比的是相对值,绝对值没有意义。
4.3 三个必调参数:节点数、超卖比、时间压缩比
第一个是节点数。它决定仿真结果的绝对量级,写错会让所有策略看起来都一样好或一样差。校准方法是:用真实数据里「最忙那一小时」的峰值资源需求,除以单机规格,得到峰值所需节点数;再用仿真跑同样的时段,如果拒绝率明显高于真实水平(差不多一个数量级),说明节点数给小了。
第二个是超卖比。生产上很少不超卖,请求值普遍虚高,画像表里的「请求/实际比」就是这个虚高程度的度量。如果这个比值在 2.5 左右,超卖比设 1.0 的仿真会严重高估资源紧张程度,得出的结论是「必须扩容」,而真实情况可能是「该收拾请求值虚高」。
第三个是时间压缩比。一天的真实负载跑仿真可能要几分钟到几十分钟,做参数扫描时不够快。常见做法是把时间轴整体除以 k(k 取 10 到 60),同时保持相对顺序和任务时长比例不变。压缩会放大调度瞬时抖动,所以压缩后只用来做策略排序,不用来报绝对等待时长。
5. 排查避坑:五条让结论翻车的细节
5.1 利用率怎么算都只有 20%,业务却在排队
现象:画像表里时间加权利用率 18% 到 22%,但业务侧的等待时长 P99 超过 20 分钟,两者对不上。
原因:利用率按整机 CPU 算,而调度是按「可分配资源」算的。一台 96 核的机器被切成几十个容器,最大的空闲块可能只有 4 核,此时整机利用率不到 60%,但一个要 8 核的任务无论如何都排不进去。这就是碎片,均值看不见它。
解决:加一个「最大空闲块」指标,按时序逐采集点统计每台机器上最大的连续空闲资源,再取分位数。如果 P50 的最大空闲块小于主力任务的请求中位数,说明瓶颈是碎片不是容量,方向应该是重排布或者改装箱策略,而不是扩机器。
5.2 短任务凭空消失,时长分布少了三分之一
现象:算出来的任务时长 P50 是 4 分钟,但业务方坚持说大部分任务几秒钟就结束了。
原因:时序表按固定间隔采集,跑不满一个间隔的任务根本没有资源记录,你在做 join 时用了 inner join,这批就被静默丢掉了。
解决:改成以任务表为主表的 left join,没有时序记录的任务保留下来并标记no_metric=True;做时长分布时用任务表自己的end_ts - start_ts,不要用时序表的记录数去反推。这一步做完,短任务占比通常会从 5% 跳到 30% 以上,很多「平均利用率」的结论会跟着变。
5.3 换了个采集间隔,同样的结论算不出来了
现象:同一集群两个时间段的数据,一段是 15 秒采集,一段是 60 秒采集,同样的分位数代码跑出来差 10 个百分点以上。
原因:分位数对采样粒度敏感。60 秒采集时,每个点是 60 秒的平均,天然平滑,P99 被压低。跨段对比等于拿两把刻度不同的尺子量同一根木头。
解决:把细粒度数据重采样到粗粒度再算,统一口径到最粗的那个间隔。重采样用max而不是mean,因为你要保留峰值语义;如果分析目标是稳态水位,就统一用mean,但必须在报告里写清楚。跨数据集做对比时更是如此,不同采集链路的聚合方式可能完全不同。
5.4 仿真跑出来的等待时间比线上好一个数量级
现象:回放真实负载,FIFO 策略的拒绝率 0.1%,线上却是 2%。
原因:大部分最小仿真器把「排队」简化为「装不下就拒绝」,而真实调度器有优先级、有抢占、有排队等待和重试。更重要的是,真实集群还混着大量非任务型的资源占用(系统组件、缓存、预留),仿真的可用容量比名义容量小。
解决:在仿真里留一个reserved_ratio参数,把可用容量按 0.85 到 0.95 折一下,再对比拒绝率。同时把「拒绝」拆成两个计数:立刻拒绝和排队超时,后者才对应线上语义。校准的判据是让 FIFO 基线的拒绝率贴近线上实测值,再在此基础上比新策略,这样相对结论才可信。
5.5 时区差八小时,整份数据的形状都歪了
现象:任务提交集中在凌晨 1 点到 5 点,白天几乎没有,跟任何真实的业务节律都对不上。
原因:提交时间戳写的是本地时间,采集时间戳写的是 UTC,两套时间混在一起,直接做diff得到的结果就是负值或者八小时的系统性偏移。国内集群这个问题尤其常见。
解决:所有时间戳在入湖时统一成 UTC 并显式带上时区,展示时再转回本地。做任何diff之前先断言:submit_ts的最小值到最大值跨度不应超过数据窗口长度,超过了就是时区没对齐。这个断言我写进了每一次分析的第一个单元格,省下来的时间不止一次。
6. 交叉验证:三个动作判断结论是不是「数据造出来的」
数据分析最危险的状态不是算错,是算出了一个漂亮但站不住的结论。我在集群数据分析上养成的习惯是,任何要写进报告的结论,都要过三个交叉验证,过不了就不写。
第一个动作,用两个独立数据源算同一个指标。比如利用率,一路从机器时序算,一路从任务表的「请求 × 时长 / 窗口长度」算。两者差异在 15% 以内算互相印证,差到 30% 以上一定有一路是错的——通常是时序覆盖不全或者任务表漏了重试。这个动作做过一次之后,你会对「机器时序可信度」有个具体数字,而不是「应该还行」。
第二个动作,做一次时间切片稳定性检验。把一整天按小时切成 24 段,每个指标各算一遍,看波动范围。如果利用率在 15% 到 60% 之间乱跳,说明你算的是峰值而不是容量水位,任何「需要扩容 20%」的结论都不成立。稳定的指标在小时尺度上波动通常在 ±10% 以内。
第三个动作,找一个「反事实」数字。比如你发现 P99 排队时长 20 分钟,先别急着归因,去算一下「如果所有任务都按请求值瞬间满足,排队时长应该是多少」。多数情况下这个下界也在 8 分钟以上,说明瓶颈根本不在调度,而在总容量或者任务粒度。这个动作能把一半的错误归因拦在报告外面。
| 验证动作 | 判定口径 | 不过关怎么办 |
|---|---|---|
| 双源一致性 | 两路结果差异 < 15% | 查时序覆盖率、查重试去重逻辑 |
| 时间切片稳定性 | 小时级波动 ±10% 内 | 改用分位数口径,剔除突发时段 |
| 反事实下界 | 实际值和下界差距有意义 | 换归因方向,先看容量和碎片 |
最后一章我想说的其实是关于习惯的。早期做这类分析,我总想着「把数据洗干净、把模型跑好」,结果每次评审被问倒的都是同一句:这个数你怎么算出来的。后来改了做法,先把画像表和三个验证动作固定在流程最前面,模型反而是最后才碰的东西。慢下来的那半天,换来的是后面几周不用返工。clusterdata 这类生产负载数据的价值,不在于它有多大,而在于它逼着人把「数据是怎么来的」这件事想清楚。希望帮到你。
本文还有配套的精品资源,点击获取