4. 数据剖析与分块策略选择

分块不是把文件机械切成若干份,而是一次基于数据特征与计算特征的工程决策。同样是 2 TB 的科研数据,按行切、按列切、按时间窗切,得到的执行效率可能相差数倍。本节要解决的问题是:在动手切分之前,先做一次数据剖析,把"数据长什么样、任务怎么读它、计算单元需要什么"这三件事量化出来,再据此确定分块维度与块大小。

一、剖析什么:三类输入特征

数据剖析的目标是产出可支撑决策的量化指标,而不是泛泛地"了解一下数据"。实践中至少需要采集三类信息。

规模与分布特征。包括总字节数、记录条数、单条记录平均长度及其方差、字段数、各字段的基数(唯一值个数)与空值率。特别要关注长度偏斜:如果单条记录长度从几十字节到几兆字节不等,按条数均分就会产生严重的长尾块。此时应统计记录长度的分位数(如 P50、P95、P99),用 P99 而非均值来估算最坏块大小。

物理组织特征。数据是行式存储(CSV、JSON Lines、二进制记录流)还是列式存储(Parquet、ORC)?是否可随机定位(有索引、有固定记录长度)还是只能顺序扫描?是否已压缩、压缩块边界在哪里?这直接决定了"能否在任意字节偏移处切开"。例如 gzip 压缩的文本流无法在压缩块内部随机切分,必须先解压或按压缩块对齐。

访问模式特征。下游任务是全表扫描、按某列过滤、按时间范围检索,还是按样本 ID 点查?过滤条件的选择率是多少?如果任务只用到 200 列中的 5 列,按列切分并配合列裁剪,I/O 量可下降一个数量级;如果任务需要按行做全局聚合,则按行切分更自然。

二、从特征到决策:分块维度与块大小的确定逻辑

下图把剖析指标、任务特征与分块决策之间的对应关系组织为一条判断链,可直接作为选型时的对照依据。

剖析指标 → 分块维度 → 块大小 的决策链 规模与分布 总字节 / 记录数 记录长度 P50 / P99 字段基数、空值率 长度方差(偏斜程度) → 决定块内数据量是否均衡 物理组织 行式 / 列式存储 可否随机定位 压缩格式与块边界 记录是否定长 → 决定切分点是否合法 访问模式 全扫描 / 过滤 / 点查 过滤列与选择率 列裁剪比例 → 决定按行切还是按列切 分块维度 按行 / 按记录区间 按列 / 按列组 按时间窗 / 按键区间 块大小 目标字节数(如 128 MB) 目标记录数 块数 ≈ 总规模 / 块大小 硬约束校验 块大小 ≥ 单条最大记录(P99 以上) 切分点对齐压缩块 / 记录边界 块数 ≥ 并行度,且不过度碎片化 均衡性校验 最大块 / 平均块 ≤ 1.5 为宜 长尾块单独拆分或二次切分 块数不宜远大于并行槽位数 注:块大小先由目标并行度反推,再用硬约束与均衡性校验修正

决策顺序建议是先定维度、再定大小。维度由访问模式主导:全扫描且需按行聚合,选行维度;宽表且只取少数列,选列维度;时序数据按时间范围检索,选时间窗维度。块大小则由并行度反推:若集群可提供 32 个并行槽位,希望每槽处理 2~4 个块以留出调度余量,则块数宜在 64~128 之间,块大小 = 总规模 / 块数。随后用硬约束修正——块大小必须大于单条最大记录,切分点必须落在合法边界上。

三、一个可复算的示例

假设某遥感影像元数据表为 JSON Lines 格式,未压缩,总大小 96 GB,共 4.8 亿条记录,平均每条 200 字节。剖析发现记录长度 P99 为 1.2 KB,存在少量超长记录;下游任务按 sensor_id 过滤后做统计,选择率约 3%,且只用到 12 个字段中的 4 个。

先按行维度估算:目标块数取 96,则块大小约 1 GB,每块约 500 万条记录。由于 P99 为 1.2 KB,1 GB 块内即使混入少量超长记录也不会显著失衡,硬约束满足。但访问模式提示可以进一步优化:任务只读 4/12 的字段,若转为列式存储并按列组切分,实际读取量可降至约 96 GB × (4/12) ≈ 32 GB,再叠加 3% 选择率,有效 I/O 更小。因此更优方案是先做一次格式转换,按列组切分为 96 个块,每块目标 1 GB,并在块内保留 sensor_id 的统计信息(min/max)以支持块级裁剪。

若数据是 gzip 压缩的,则不能直接按 1 GB 字节偏移切分,必须先解压或按压缩成员边界对齐。此时可用如下方式先探测合法切分点:

# 示例:统计 gzip 成员边界,确认可切分位置(示例命令,非实测输出)
python3 - <<'PY'
import zlib
path = "meta.jsonl.gz"
CHUNK = 8 * 1024 * 1024
pos, boundaries = 0, []
with open(path, "rb") as f:
    d = zlib.decompressobj(16 + zlib.MAX_WBITS)
    while True:
        buf = f.read(CHUNK)
        if not buf:
            break
        d.decompress(buf)
        if d.eof:                      # 到达一个 gzip 成员结尾
            boundaries.append(pos + len(buf) - len(d.unused_data))
            d = zlib.decompressobj(16 + zlib.MAX_WBITS)
        pos += len(buf)
print("可切分边界数:", len(boundaries))
PY

得到边界列表后,把相邻边界之间的区间合并到接近 1 GB 的块,即可保证每个块都能独立解压,避免跨块读取。

四、常见误判与修正

  • 用平均值定块大小。均值掩盖长尾,遇到 P99 远大于均值的记录会导致个别块超时。应以 P99 或最大值参与硬约束校验。
  • 块数越多越好。块数远超并行槽位会带来调度与元数据开销,小文件问题在对象存储上尤其明显。块数控制在槽位数的 2~4 倍较稳妥。
  • 忽略压缩边界。对 gzip、zstd 流直接按字节切分会产生不可解压的残块,必须按成员或帧边界对齐。
  • 维度选错再调大小。若任务只需少数列却按行切分,无论块大小怎么调,I/O 浪费都无法消除,应先改维度。
把剖析结果固化成一份参数表(总规模、记录数、P99 长度、压缩格式、过滤列、选择率、并行槽位数、目标块数、块大小、切分点规则),后续同类数据集可直接复用这套决策,而不必每次重新试错。

小结:分块策略的合理性取决于剖析是否到位。先量化规模分布、物理组织与访问模式,再按"维度优先、大小反推、约束校验"的顺序落地,最后用均衡性与合法性两项检查收口,才能让分块真正服务于计算任务,而不是制造新的瓶颈。