第 3 章 面向嵌入式的资源受限写入优化

3.1 问题分析与设计目标

嵌入式设备、容器限额节点和边缘计算终端等资源受限环境对 TsFile 写入提出了严格的内存约束。在这些场景中,可用内存通常仅为数十 MB,而写入的时间序列数据量可能达到数亿行。用户面临以下困境:

  • 不知道每次应写入多少行(批次大小 $F$);
  • 不知道写入多少次后需要调用 flush() 将数据落盘;
  • 不知道何时内存将超出上限,需要关闭当前文件并打开新文件。

现有的 TsFileWriter / TsFileTableWriter 将上述三个决策完全交给用户,使用者需要深入理解 TsFile 内部的内存模型才能正确配置参数。

设计目标

  1. 约束驱动:用户只需声明内存上限和 schema,系统自动推导所有写入参数。
  2. 零决策负担:用户只调用 write_table(),flush / close / 文件轮转全部自动处理。
  3. 构造时可行性校验:约束不可满足时立即报错,并说明最小可用配置。
  4. 运行时可观测:提供当前内存用量估算、已写行数、已创建文件数等运行时状态。
  5. 零运行时监控开销:不依赖原子操作级别的内存追踪,仅使用轻量整数计算。

3.2 TsFile 写入过程内存模型分析

3.2.1 内存组成

通过对写入过程的逐模块内存追踪实验(基于 ModStat 编译期统计框架),确认 TsFile 写入过程中的内存由以下模块组成:

模块标识说明特征
CW_PAGES_DATAChunkWriter 已编码的数据页主导项,占峰值 80-85%,flush 后清零
DEFAULT通用分配(ChunkWriter 对象、ByteStream 等)随活跃设备数分配,设备切换时部分释放
TABLET用户提交的 Tablet 数据缓冲每个 batch 分配/释放,生命周期短
PAGE_WRITER_OUTPUT_STREAMPageWriter 编码输出流随活跃设备数分配,周期性释放
STATISTIC_OBJ统计对象(min/max/count 等)少量,随 flush 次数线性增长
ENCODER_OBJ / COMPRESSOR_OBJ编码器、压缩器对象少量,随 flush 次数线性增长
TSFILE_WRITER_METAChunkMeta、ChunkGroupMeta 等元数据不可压缩,单调递增,仅 close 时释放
TS2DIFF_OBJTS_2DIFF 编码差分缓冲少量

这些模块可归纳为三类:

$$M_{\text{total}} = M_{\text{init}} + M_{\text{data}} + M_{\text{meta}}$$

组成来源释放时机能否控制
$M_{\text{init}}$固定开销(schema 注册、Writer 对象等)程序结束不可控
$M_{\text{data}}$ChunkWriter 数据页(CW_PAGES_DATA + DEFAULT + TABLET 等)flush 后立即释放控制批次大小 $F$
$M_{\text{meta}}$ChunkGroup/Chunk 元数据(TSFILE_WRITER_META仅 close 时释放控制每文件 flush 次数 $K$

关键洞察:$M_{\text{meta}}$ 是核心约束来源。只要文件不 close,每次 flush 都会向内存中追加一批不可压缩的元数据。设备数越多、flush 次数越多,$M_{\text{meta}}$ 积压越严重。因此,文件轮转的根本原因不是磁盘空间,而是为了释放 $M_{\text{meta}}$

3.2.2 内存量化模型

对于 Table 模式,各量的计算方式如下:

每行数据的内存字节数

$$s_{\text{tablet}} = 8 + \sum_{\text{all cols}} \text{sizeof}(\text{col_type})$$

$$s_{\text{data}} = \sum_{\text{FIELD cols}} (8 + \text{sizeof}(\text{field_col_type}))$$

$$s = s_{\text{tablet}} + s_{\text{data}}$$

其中 $s_{\text{tablet}}$ 是用户 Tablet 对象占用(包含时间戳 8 字节 + 所有列),$s_{\text{data}}$ 是 ChunkWriter 内部对 FIELD 列的编码缓冲(每列 8 字节 page header + 数据本身)。

每设备每次 flush 的元数据增量

$$b = n_{\text{field_cols}} \times 104 + 96 \quad \text{(bytes/设备/flush)}$$

其中 104 bytes 来自每个 ChunkMeta 的序列化大小,96 bytes 来自 ChunkGroupMeta。

三类内存的量化公式

组成公式说明
$M_{\text{init}}$$\approx 900$ KB固定,实测值
$M_{\text{data}}$$s \times F$$F$ = 当前 flush 区间内已写行数
$M_{\text{meta}}$$K \times D \times b$$K$ = 已 flush 次数,$D$ = 设备数

类型字节数映射:

TSDataType字节数
BOOLEAN1
INT32 / FLOAT4
INT64 / DOUBLE8
STRING32(保守估计,实际为变长)

3.2.3 模型验证

编译期内存追踪框架

TsFile C++ 实现了基于编译选项 ENABLE_MEM_STAT 的内存追踪框架。其核心组件 ModStat 类通过 cache-line 对齐的原子计数器(ATOMIC_FAA)为每个分配模块(AllocModID)维护实时内存占用统计。所有内存分配通过 mem_alloc(size, mid) / mem_free(ptr) 接口完成,在分配的 8 字节头中记录模块 ID 和大小,从而实现零侵入的逐模块追踪。

该框架仅在编译时开启,不影响生产构建的性能。

实验设计

实验程序(experiment/write_memory)配置如下:

  • Schema:1 张表(mem_bench),2 个 TAG 列(STRING)+ 6 个 FIELD 列(INT64, DOUBLE, FLOAT, INT32, INT64, INT32),SNAPPY 压缩
  • 数据规模:200M 行,10 个设备,每设备 20M 行
  • 内存阈值:50 MB(memory_threshold 参数)
  • 采集频率:每 50 万行记录一次各模块内存,输出 CSV
  • 写入模式:两种——顺序写入(按设备逐个写完)和混合写入(每个 Tablet 包含所有设备)

顺序写入模式结果

顺序写入模式下,每个 Tablet 仅包含单个设备的数据,10 个设备依次写完。

顺序写入内存曲线

Flush 事件触发行数Flush 前Flush 后释放量
#132.1M41.5 MB8.1 MB33.4 MB
#270.0M42.3 MB8.1 MB34.2 MB
#3104.1M48.1 MB8.2 MB39.8 MB
#4133.5M38.7 MB8.0 MB30.7 MB
#5171.0M38.8 MB7.4 MB31.4 MB
#6 (final)200.0M26.2 MB0.1 MB26.1 MB
  • 峰值内存:48.1 MB
  • 写入吞吐量:3.36 M rows/s
  • CPU 利用率:稳定约 115%

顺序模式的特征:在每 20M 行的设备切换边界处,DEFAULT(4.5 → 0.8 MB)和 TABLET(2.75 → 0.48 MB)出现短暂凹陷,形成锯齿状小波动。这是因为旧设备的 Tablet 对象和临时缓冲区被析构,新设备的尚未分配。CW_PAGES_DATA 则不受设备切换影响,持续线性增长直到触发 flush。

混合写入模式结果

混合写入模式下,每个 Tablet 包含全部 10 个设备的数据(每设备 6553 行,按设备分组连续排列)。

混合写入内存曲线

Flush 事件触发行数Flush 前Flush 后释放量
#129.0M41.8 MB12.9 MB29.0 MB
#258.1M41.8 MB7.4 MB34.5 MB
#389.1M41.7 MB12.8 MB28.9 MB
#4127.5M42.0 MB7.4 MB34.6 MB
#5166.1M47.6 MB12.2 MB35.4 MB
#6198.6M48.6 MB12.0 MB36.6 MB
#7 (final)200.0M6.4 MB0.1 MB6.2 MB
  • 峰值内存:48.6 MB
  • 写入吞吐量:3.30 M rows/s
  • CPU 利用率:稳定约 115%

两种模式的关键差异

对比项顺序写入混合写入
Flush 后残留内存~8 MB~12 MB
自动 flush 次数5 次6 次
设备切换锯齿有(每 20M 行)
PAGE_WRITER_OUTPUT_STREAM 峰值0.46 MB(1 个设备活跃)4.59 MB(10 个设备同时活跃)
更接近场景批量导入历史数据实时采集(多设备数据交替到达)

混合模式下 flush 后残留更高(~12 MB vs ~8 MB),因为所有 10 个设备的 ChunkWriter 同时活跃,其 DEFAULT 分配和 PAGE_WRITER_OUTPUT_STREAM 缓冲在 flush 后仍保留(ChunkWriter 对象本身不销毁,只清空数据页)。

模型验证结论

配置:50 设备,50 FIELD 列 int32,总行数 100,000:

max_rows实测内存公式估算误差
10,00011.7 MB11.05 MB5.5%
8,00010.8 MB10.3 MB4.6%
6,00010.6 MB9.63 MB9.1%
5,00011.2 MB9.75 MB12.9%

公式误差 5-15%,主要来源:变长字符串开销、schema 结构节点、OS 内存分配粒度。该误差范围可通过安全裕度系数覆盖。


3.3 资源约束下的自适应参数配置方案

3.3.1 EOQ 最优策略(总量已知)

当写入总量 $R$(每设备总行数)和设备数 $D$ 均已知时,问题等价于经济订货量(EOQ)模型:$M_{\text{data}}$ 和 $M_{\text{meta}}$ 存在此消彼长的关系——增大批次大小 $F$ 可减少 flush 次数 $K$,降低 $M_{\text{meta}}$,但增大 $M_{\text{data}}$;反之亦然。

最优解使两者在峰值时刻相等:

$$F_{\text{opt}} = \sqrt{\frac{R \times D \times b}{s}}$$

$$K_{\text{opt}} = \frac{R}{F_{\text{opt}}}$$

$$M_{\min} = M_{\text{init}} + 2 \sqrt{R \times s \times D \times b}$$

当 $M_{\min} \leq M_{\text{limit}}$ 时,所有数据可写入单个文件,$F_{\text{opt}}$ 即为最优批次大小。

3.3.2 对半分预算策略(总量未知)

在流式写入场景中,总量未知,无法使用 EOQ 公式。此时采用预算对半分策略,使单个文件容纳的数据量 $F \times K$ 最大化:

$$M_{\text{avail}} = M_{\text{limit}} - M_{\text{init}}$$

$$F = \frac{M_{\text{avail}}}{2 \times s}, \quad K = \frac{M_{\text{avail}}}{2 \times D \times b}$$

该策略将可用内存均匀分配给 $M_{\text{data}}$ 和 $M_{\text{meta}}$,是数学上使 $F \times K$ 最大的分配方案。

3.3.3 可行性检查

在构造 Writer 时执行可行性校验:

$$M_{\min} = M_{\text{init}} + s + b$$

若 $M_{\min} > M_{\text{limit}}$,则当前 schema 在给定内存约束下不可行,应立即报错并说明最小可用配置。

3.3.4 策略推导算法

输入:M_limit, schema, n_devices, total_rows_per_device

1. 从 schema 计算 s, b
2. 可行性检查:M_init + s + b ≤ M_limit,否则报错
3. M_avail = M_limit - M_init
4. 若 total_rows > 0 且 n_devices > 0(总量已知):
     B = n_devices × b
     F = √(total_rows × B / s)
     K = total_rows / F
     peak = M_init + s×F + K×B
     若 peak ≤ M_limit:返回 {F, K, peak}   // EOQ 最优
     否则退化为对半分
5. 对半分策略:
     D_eff = max(1, n_devices)
     F = M_avail / (2 × s)
     K = M_avail / (2 × D_eff × b)
     返回 {F, max(1, K), M_init + s×F + K×D_eff×b}

3.4 资源感知的主动刷盘与文件轮转控制

3.4.1 两级内存控制机制

$M_{\text{data}}$ 和 $M_{\text{meta}}$ 的释放时机不同,因此需要两级控制:

  • 第一级:flush 控制——当 $M_{\text{data}}$ 达到预算时自动 flush,将编码后的数据页写入磁盘,释放 CW_PAGES_DATA 等缓冲。
  • 第二级:文件轮转控制——当 $M_{\text{meta}}$ 达到预算时关闭当前文件、打开新文件,释放不可压缩的元数据。
write_table(tablet):
  writer->write_table(tablet)                 // M_data 增加
  accumulated_rows += tablet.rows

  if accumulated_rows >= F:                   // 达到数据预算
      flush()                                 // M_data 清零,M_meta 增加 D×b
      flush_count++
      accumulated_rows = 0
      if flush_count × D × b >= rotate_thresh:
          rotate_file()                       // M_meta 清零

两者结合后,$M_{\text{total}}$ 始终受控:

阶段内存状态
写入中(未达 $F$)$M_{\text{init}} + s \times \text{accumulated} + \text{flush_count} \times D \times b$
flush 后$M_{\text{init}} + \text{flush_count} \times D \times b$
rotate 后$M_{\text{init}}$

3.4.2 安全裕度设计

由于量化模型存在 5-15% 的估算误差,文件轮转的触发条件引入安全裕度系数:

$$\text{rotate_thresh} = \text{meta_budget} \times \text{SAFETY_FACTOR}$$

其中 $\text{SAFETY_FACTOR} = 0.85$,即提前 15% 触发轮转。

data_budget  = s × F
meta_budget  = M_avail - data_budget
rotate_thresh = meta_budget × 0.85

3.4.3 关闭时内存峰值分析

close() 调用时,$M_{\text{data}}$ 已为 0(最后一次 flush 已完成)。峰值为:

$$M_{\text{at_close}} = M_{\text{init}} + M_{\text{meta_final}}$$

由于在 $\text{flush_count} \times D \times b \geq \text{rotate_thresh}$ 时触发 rotate:

$$M_{\text{meta_final}} \leq \text{meta_budget} \times \text{SAFETY_FACTOR} \leq \text{meta_budget} \leq M_{\text{avail}}$$

$$\Rightarrow M_{\text{at_close}} \leq M_{\text{init}} + M_{\text{avail}} = M_{\text{limit}} \quad \checkmark$$

关闭时不会超出内存上限。

3.4.4 已有自动刷盘机制的协同

TsFileWriter 内部已有一个自适应的 $M_{\text{data}}$ 检查机制,通过 chunk_group_size_threshold_ 控制:

int TsFileWriter::check_memory_size_and_may_flush_chunks() {
    if (record_count_since_last_flush_ >= record_count_for_next_mem_check_) {
        int64_t mem_size = calculate_mem_size_for_all_group();
        record_count_for_next_mem_check_ =
            record_count_since_last_flush_ *
            chunk_group_size_threshold_ / mem_size;
        if (mem_size > chunk_group_size_threshold_) {
            flush();
        }
    }
}

该机制通过动态调整检查间隔实现自适应——当内存增长速度快时检查更频繁,慢时检查更稀疏。TsFileTableWriter 在构造时将 memory_threshold 参数直接赋值给 chunk_group_size_threshold_

TsFileTableWriter(WriteFile* writer_file, T* table_schema,
                  uint64_t memory_threshold = 128 * 1024 * 1024) {
    // ...
    common::g_config_value_.chunk_group_size_threshold_ = memory_threshold;
}

MemConstrainedWriter 的设计中,flush 由上层根据累积行数 $F$ 显式触发。底层的 chunk_group_size_threshold_ 设为 data_budget 值,作为双重保险——若单个超大 Tablet 导致 $M_{\text{data}}$ 短暂超出预算,底层机制仍可兜底触发 flush。

3.4.5 从公式估算到直接监控的演进

TsFile 已有两个可直接测量内存的接口,可替代公式估算:

$M_{\text{data}}$ 的直接测量TsFileWriter::calculate_mem_size_for_all_group() 遍历所有 ChunkWriter,返回编码后的实际数据大小:

int64_t estimate_max_series_mem_size() {
    return chunk_data_.total_size()              // 已完成页(编码后字节)
         + page_writer_.estimate_max_mem_size()  // 当前页已编码 + encoder pending 上界
         + page_header_overhead + statistic_sizeof;
}

测量的是编码后的实际大小,高效编码时返回值远小于 $s \times F$,天然适应实际编码效率。

$M_{\text{meta}}$ 的直接测量:所有元数据均从 TsFileIOWriter::meta_allocator_(类型 PageArena)分配,只需给 PageArena 新增 get_total_used_bytes() 方法即可获得精确的 $M_{\text{meta}}$ 大小。

有了直接监控后,运行时控制简化为纯粹的阈值比较:

write_table(tablet):
  writer->write_table(tablet)
  if writer->get_data_size() >= data_budget:
      writer->flush()
      if writer->get_meta_size() >= meta_budget:
          rotate_file()

原来需要计算的 $F$、$K$、$s$、$b$、$D$ 等参数大部分消失,“最优策略”退化为唯一一个决策:如何在 $M_{\text{avail}}$ 中分配 data_budget 和 meta_budget

3.4.6 设备数未知时的自适应追踪

当用户未指定设备数(流式写入场景常见)时,系统进入动态追踪模式:

// 每次 write_table 后
d_in_tablet = tablet.find_all_device_boundaries().size() - 1
if d_in_tablet > observed_max_devices:
    observed_max_devices = d_in_tablet
    recompute_rotate_thresh()     // D 增大 → K 减小 → thresh 减小

recompute_rotate_thresh():
    D = max(1, observed_max_devices)
    rotate_thresh = meta_budget / (D × b) × SAFETY_FACTOR

observed_max_devices 在单个文件内单调递增(rotate 后重置为 0),设备数越多阈值越小,天然保守。


3.5 MemConstrainedWriter 接口设计

3.5.1 核心数据结构

struct WritingStrategy {
    int64_t rows_per_flush;        // F:每次 flush 的行数
    int64_t flushes_per_file;      // K:每文件最多 flush 次数
    int64_t estimated_peak_mem;    // 按此策略的预估峰值内存(bytes)
};

struct WriteStats {
    int64_t rows_written_total;           // 累计写入行数(跨所有文件)
    int64_t files_completed;              // 已完成(已 close)的文件数
    int64_t current_file_flush_count;     // 当前文件已 flush 次数
    int64_t estimated_current_mem_bytes;  // 当前估算内存
};

3.5.2 接口设计

class MemConstrainedWriter {
   public:
    static constexpr int DEVICES_UNKNOWN = 0;

    MemConstrainedWriter(
        int64_t mem_limit_bytes,
        const std::string& output_dir,
        const std::string& file_prefix,
        const std::shared_ptr<TableSchema>& table_schema,
        int n_devices = DEVICES_UNKNOWN,
        int64_t total_rows_per_device = -1
    );

    ~MemConstrainedWriter();

    int init_status() const;

    // 写入接口(内部自动 flush / close / reopen)
    int write_table(Tablet& tablet);
    int close();

    // 查询建议的 Tablet 行数
    int64_t rows_per_tablet() const;

    // 查询当前策略与运行时状态
    const WritingStrategy& strategy() const;
    const WriteStats& stats() const;
    const std::vector<std::string>& created_files() const;

    // 纯计算接口:不创建文件,仅返回策略
    static WritingStrategy compute_strategy(
        int64_t mem_limit_bytes,
        const std::shared_ptr<TableSchema>& table_schema,
        int n_devices,
        int64_t total_rows_per_device = -1
    );
};

文件按 {output_dir}/{file_prefix}_{index:04d}.tsfile 命名,如 /data/output/sensor_0000.tsfile

3.5.3 write_table 流程

用户提交任意大小的 Tablet,系统内部累计行数,达到 $F$ 时自动 flush,达到 $K$ 次 flush 后自动轮转文件:

write_table(tablet):
  if init_status != E_OK: return error

  writer->write_table(tablet)
  accumulated_rows += tablet.rows

  // 动态设备追踪
  if dynamic_mode:
    d = count_devices_in_tablet(tablet)
    if d > observed_max_devices:
        observed_max_devices = d
        recompute_rotate_thresh()

  if accumulated_rows >= F:
      do_flush()

do_flush():
  writer->flush()
  accumulated_rows = 0
  flush_count++

  meta_estimate = flush_count × D_eff × b
  if meta_estimate >= rotate_thresh:
      rotate_file()

rotate_file():
  writer->close()
  file_index++
  writer = new TsFileWriter()
  writer->open(next_file_path)
  writer->register_table(schema)
  flush_count = 0
  if dynamic_mode: observed_max_devices = 0

3.5.4 使用示例

基本用法

auto schema = std::make_shared<TableSchema>("sensor_table", columns);

MemConstrainedWriter writer(
    10 * 1024 * 1024,   // 10 MB 内存上限
    "/data/output",
    "sensor",
    schema,
    50,                 // 50 个设备
    100000              // 每设备总行数(可选)
);

if (writer.init_status() != E_OK) {
    return writer.init_status();   // 内存不足以写入此 schema
}

int F = writer.rows_per_tablet();

while (has_more_data) {
    Tablet tablet(..., F);
    fill(tablet);
    writer.write_table(tablet);   // flush / rotate 全自动
}
writer.close();

仅查询策略(不创建文件):

auto s = MemConstrainedWriter::compute_strategy(
    10 * 1024 * 1024, schema, 50, 100000);
// s.rows_per_flush     = 每批次建议行数
// s.flushes_per_file   = 每文件建议 flush 次数
// s.estimated_peak_mem = 预估峰值内存

3.5.5 约束与局限

  1. 超大 Tablet 的短暂 $M_{\text{data}}$ 超限:若单个 Tablet 行数超过 $F$,$M_{\text{data}}$ 会短暂超出预算。底层 chunk_group_size_threshold_ 作为双重保险兜底。
  2. STRING 列内存估算误差:使用固定 32 字节估算变长字符串,通过 SAFETY_FACTOR 缓解。
  3. Schema 同构假设:假设所有设备使用相同的 FIELD 列结构。异构 schema 取最大 FIELD 列数作为保守估计。
  4. chunk_group_size_threshold_ 是进程全局配置:多个 Writer 实例并发时会互相覆盖,当前设计仅支持单写入器场景。
  5. 单线程写入:不考虑并发写入。

3.6 实验与评估

3.6.1 实验环境与数据集

  • 硬件:Apple Silicon(macOS Darwin 24.6.0)
  • 编译:Release 模式,ENABLE_MEM_STAT=ON
  • Schema:1 张表,2 TAG(STRING)+ 6 FIELD(INT64, DOUBLE, FLOAT, INT32, INT64, INT32),SNAPPY 压缩
  • 数据量:200M 行,10 个设备
  • 内存阈值:50 MB
  • Batch 大小:65,536 行
  • 采集间隔:每 50 万行记录一次内存快照(含 wall time、user CPU、sys CPU)

3.6.2 内存控制精度

两种写入模式下,50 MB 阈值的实际控制效果:

指标顺序写入混合写入
设定阈值50 MB50 MB
实测峰值48.1 MB48.6 MB
峰值 / 阈值96.2%97.2%
Flush 后最低7.4 MB7.4 MB
自动 flush 次数56

内存峰值均未超过设定阈值,说明 chunk_group_size_threshold_ 的自适应检查机制有效。峰值略低于 50 MB 是因为检查粒度为 batch 级别(64K 行),在接近但未达阈值时即完成当前 batch 并触发 flush。

3.6.3 写入吞吐量

模式吞吐量CPU 利用率
顺序写入3.36 M rows/s~115%
混合写入3.30 M rows/s~115%

两种模式吞吐量接近。CPU 利用率 >100% 表明部分编码/压缩工作利用了多核(SNAPPY 压缩开销)。

作为参考,同等规模下 Parquet(Arrow + SNAPPY)的写入吞吐约 6.3 M rows/s。TsFile 写入的主要瓶颈在于 Tablet::add_value() 的按列名 string map 查找开销,而非 I/O 或编码本身。

3.6.4 行为示例

以 50 设备、50 FIELD 列 int32、$M_{\text{limit}} = 12$ MB 为例:

s = 1008 bytes/row
b = 50 × 104 + 96 = 5296 bytes/设备/flush
M_avail ≈ 11.5 MB

对半分策略:
  F = 11.5 MB / (2 × 1008) ≈ 5,694 行
  K = 11.5 MB / (2 × 50 × 5296) ≈ 21 次

rotate_thresh = 5.75 MB × 0.85 / (50 × 5296) ≈ 第 18 次 flush
事件$M_{\text{meta}}$ 估算操作
flush 10.26 MB继续
flush 102.65 MB继续
flush 184.77 MB $\geq$ threshrotate:close 释放 4.77 MB 元数据,重开新文件
flush 1(新文件)0.26 MB继续

文件轮转的本质:每 18 次 flush 必须 close 一次,释放积压的不可压缩元数据。


3.7 本章小结

本章针对嵌入式环境下 TsFile 写入的内存约束问题,提出了一套完整的资源受限写入优化方案:

  1. 内存模型:将写入过程内存分解为 $M_{\text{init}}$(固定)、$M_{\text{data}}$(周期性,flush 释放)和 $M_{\text{meta}}$(单调递增,close 释放)三部分,并建立了量化公式。通过 ModStat 编译期追踪框架进行了实验验证,公式误差在 5-15% 范围内。

  2. 参数配置策略:针对总量已知和未知两种场景,分别给出了 EOQ 最优策略和对半分预算策略,实现了自动推导批次大小 $F$ 和文件轮转频率 $K$。

  3. 两级内存控制:通过 flush(释放 $M_{\text{data}}$)和文件轮转(释放 $M_{\text{meta}}$)的两级控制机制,配合安全裕度设计,保证写入全程内存不超过用户设定的上限。同时与 TsFile 已有的 chunk_group_size_threshold_ 自适应刷盘机制形成双重保险。

  4. MemConstrainedWriter 接口:封装了上述所有机制,用户只需提供内存上限即可实现零决策的不间断写入,支持自动 flush、自动文件轮转和动态设备追踪。

  5. 实验验证:在 200M 行、50 MB 阈值的实验中,两种写入模式均将内存峰值控制在阈值以内,写入吞吐达到 3.3 M rows/s,验证了方案的有效性和实用性。