资源约束下的 TsFile 读写优化——论文结构草案

状态:讨论用草案,各章节标注置信度和待确认项 核心叙事:以资源约束维度为轴,依次呈现内存受限、CPU 并行、向量化加速、AI 接口四个维度的优化方案


第 1 章 绪论(略)


第 2 章 背景与 TsFile 概述(略)


第 3 章 内存受限下的 TsFile 读写优化

约束:可用内存严格有限(数十 MB),写入数据量可达数亿行 置信度:高——现有 sec3.md 内容扎实,主要补充读侧

3.1 TsFile 写入过程内存模型

  • 三分模型:$M_{\text{total}} = M_{\text{init}} + M_{\text{data}} + M_{\text{meta}}$
  • 各模块内存量化公式(每行字节数 $s$,每次 flush 元数据增量 $b$)
  • 关键洞察:TSFILE_WRITER_META 单调递增,flush 不释放,close 才释放
  • ModStat 编译期内存追踪框架(ENABLE_MEM_STAT

3.2 写入内存实验

  • 顺序写入 vs 混合写入的内存曲线对比
  • 峰值分析、flush 后残留差异、设备切换锯齿效应
  • 公式验证:误差 5-15%,来源分析

3.3 内存约束下的自适应参数配置

  • EOQ 最优策略(总量已知):$F_{\text{opt}} = \sqrt{R \times D \times b / s}$
  • 对半分预算策略(流式,总量未知)
  • 可行性检查:构造时报错,说明最小可用配置
  • 安全裕度设计(SAFETY_FACTOR = 0.85)

3.4 两级刷盘与文件轮转控制

  • 第一级:flush 释放 $M_{\text{data}}$
  • 第二级:rotate 释放 $M_{\text{meta}}$
  • 与底层 chunk_group_size_threshold_ 的协同(双重保险)
  • 动态设备数追踪(observed_max_devices

3.5 MemConstrainedWriter 接口设计

  • WritingStrategy / WriteStats 数据结构
  • 核心接口:write_table()compute_strategy()(静态)
  • 使用示例与约束说明

3.6 读取路径内存管理

状态:已确认可用 ModStat 追踪(alloc 层不区分读写,读取路径同样接入);约束接口只控制 batch_size,不控制并发列数。

3.6.1 读取内存模型

读取全程内存由以下组件构成:

组件公式释放时机
TsBlock 输出缓冲$\text{batch_size} \times s_{\text{row}}$每批次返回后
解压缓冲(per column)$N_{\text{cols}} \times C_{\text{page}}$每 Page 解码后
Decoder / Compressor 对象$N_{\text{cols}} \times c_{\text{dec}}$(小量)查询结束
PageArena 临时分配随查询涉及的 Chunk/Page 数增长查询结束一次性回收
LRU 元数据缓存$\leq 1000 \times \bar{b}_{\text{meta}}$(固定上限)进程生命周期

其中:

  • $s_{\text{row}} = 8 + \sum_{\text{cols}} \text{sizeof}(\text{col_type})$(时间戳 8 字节 + 各列)
  • $C_{\text{page}}$:每列解压缓冲,由 Page 大小决定(约 64 KB,可配置)

整体估算:

$$M_{\text{read}} \approx M_{\text{fixed}} + \text{batch_size} \times s_{\text{row}} + N_{\text{cols}} \times C_{\text{page}}$$

其中 $M_{\text{fixed}}$ 包含 LRU 缓存、Decoder 对象等固定开销。

关键结论:读取内存主要由列数和 batch_size 驱动,远比写侧结构简单(无元数据单调增长问题)。

3.6.2 ModStat 实验

实验设计:固定设备数和时间范围,变化以下两个维度:

  • 列数 $N_{\text{cols}}$:1 / 4 / 8 / 16
  • batch_size:1K / 4K / 16K / 64K 行

记录各 AllocModID 的峰值内存,验证上述量化公式。

3.6.3 资源约束读取接口

给定内存上限 $M_{\text{limit}}$ 和查询 schema,自动推导 batch_size:

$$\text{batch_size} = \left\lfloor \frac{M_{\text{limit}} - M_{\text{fixed}} - N_{\text{cols}} \times C_{\text{page}}}{s_{\text{row}}} \right\rfloor$$

接口形式:

// 纯计算:给定内存上限和 schema,返回建议的 batch_size
int64_t compute_read_batch_size(
    int64_t mem_limit_bytes,
    const std::vector<ColumnSchema>& columns
);

// 或作为 query() 的重载,内部自动使用推导的 batch_size
reader.query(table_name, columns, start_time, end_time,
             result_set, filter, mem_limit_bytes);

用户不再需要手动调参,只需声明内存上限,系统保证每批次内存不超限。

:并发解码(ENABLE_THREADS)不引入额外内存消耗——各列解压缓冲已独立分配,并发只影响 CPU 调度,不影响内存总量。因此约束接口无需感知线程数。

3.7 本章小结


第 4 章 CPU 并行化:多线程读写框架

约束:内存充足,目标是充分利用多核 CPU 置信度:中高——读写两侧并行模型已确认,实验数据待补充

4.1 统一并行模型

读写两侧共享同一个并行设计哲学:

主线程负责串行的协调性工作(无法或不值得并行化的部分):

  • 写侧:Tablet 拆分、设备路由、内存检查、flush 落盘
  • 读侧:元数据查找、文件 I/O(顺序读取,保证有序性)、TsBlock 组装

线程池负责 CPU 密集的 per-measurement 任务(完全独立,天然可并行):

  • 写侧:各测点(measurement)的编码任务
  • 读侧:各列(column)的解压 + 解码任务

并行粒度统一在测点/列层面,而非设备层面。这一设计的关键推论:

  • 线程数应配置为 field 列数(或其合理上限),而非固定值
  • 线程安全由数据隔离保证(每个测点/列拥有独立的 ChunkWriter / ValueColumnState),无需锁同步
  • 条件编译 ENABLE_THREADS 关闭后完全移除线程依赖,回退到串行路径(嵌入式适配)

4.2 ThreadPool 设计

  • 可配置大小的工作线程模型(建议 = field 列数)
  • 双条件变量:cv_work_(唤醒空闲线程)/ cv_done_wait_all() 同步)
  • 活跃计数 active_:原子递增/递减,wait_all() 等待 active_ == 0
  • 写侧使用 submit() + future.get() 获取结果;读侧使用 submit() + wait_all()

4.3 写入路径并行化

执行模型

主线程
  │
  ├─ split_tablet_by_device()        串行:Tablet 按 TAG 列分组
  │
  ├─ 对每个设备:
  │    ├─ time_write_column_batch()  串行:时间列写入(共享)
  │    └─ 对每个 field 列:
  │         └─ pool.submit(          并行:各测点独立编码
  │              value_write_column_batch(col_i))
  │    └─ pool.wait_all()
  │
  └─ check_memory_and_may_flush()
       └─ flush()                    串行:单线程落盘
  • 线程安全保证:每个测点拥有独立的 ValueChunkWriter 实例,编码缓冲、统计对象均不共享
  • flush 保持串行:end_encode_chunk() → 磁盘 I/O → reset() 三步不可打断,flush 期间无新编码任务提交

flush 串行瓶颈分析与双缓冲可行性

FLUSH_CHUNK 宏将 CPU 工作(end_encode_chunk())与磁盘 I/O(flush_chunk())串行绑定,reset() 必须等 I/O 完成后才能释放 ChunkWriter 供下一批写入。这意味着 flush 期间线程池完全空闲,是潜在的吞吐瓶颈。

理论上可通过双缓冲消除此瓶颈:每个 ChunkWriter 持有 active/sealed 两套 ByteStream,flush 触发时 swap 两套缓冲,主线程立即可将新数据写入 active,后台 I/O 线程读 sealed 写盘。其可行性条件已满足(各 ChunkWriter 实例独立,ByteStream 支持 swap),工程实现不在本文范围内,作为后续优化方向。

4.4 读取路径并行化

两阶段模型

主线程(Phase 1:串行 I/O)
  │
  ├─ 对每个 column:
  │    └─ ensure_value_page_loaded(col)   从文件读取压缩数据到内存
  │
  └─ 提交并行任务(Phase 2:并行 CPU)
       ├─ pool.submit(decompress_and_parse(col_0))
       ├─ pool.submit(decompress_and_parse(col_1))
       │    ├─ compressor->uncompress()
       │    ├─ parse null bitmap
       │    └─ decoder->reset()
       └─ pool.wait_all()
  • Phase 1 串行保证文件访问有序,避免并发文件句柄竞争
  • Phase 2 各列任务完全独立(ValueColumnState 独立实例),无锁
  • 代码路径:AlignedChunkReader::decode_cur_value_pages_multi()

4.5 两级并发配置

并发控制采用编译期 + 运行时两级门控,在 global.cc 初始化,通过 global.h 的 setter/getter 暴露:

编译期(ENABLE_THREADS)
  OFF → 不引入任何线程库,完全串行,适合裸机嵌入式
  ON  → 线程基础设施可用,由运行时配置决定是否启用
          │
          ▼
运行时(g_config_value_)
  parallel_read_enabled_   = true   读取并行开关
  parallel_write_enabled_  = true   写入并行开关
  read_thread_count_       = 4      读取线程数上限(1-64)
  write_thread_count_      = 6      写入线程数上限(1-64)

有效线程数 = $\min(\texttt{thread_count}, N_{\text{field}})$:配置的上限与实际任务数取小,避免空转线程的调度开销。

配置建议

  • 高性能服务器:thread_count = CPU 核心数,enabled = true
  • 列数固定的场景:thread_count = field 列数,精确匹配任务数
  • 嵌入式环境:编译时关闭 ENABLE_THREADS,运行时配置无意义

4.6 实验评估

固定 field 列数,对比三种工作模式:

模式配置说明
串行基线ENABLE_THREADS=OFFenabled=false无线程调度开销
线程不足thread_count < field 列数线程复用,部分并行
线程充足thread_count $\geq$ field 列数每列一个线程,最大并行

分别在写入读取两个路径上采集上述三种模式的吞吐量,field 列数取 4 / 8 / 16 作为代表场景。

4.7 本章小结


第 5 章 SIMD 向量化加速

约束:目标平台支持 SIMD 指令(x86 AVX2 / ARM NEON) 状态:范围已确认——时间过滤(不含值过滤);写侧差分计算和位打包待实现;批量化接口已就绪

5.1 批量化接口:SIMD 的前提

SIMD 操作的基本要求是连续内存数组。TsFile 写入路径已构建完整的批量化接口层:

TsFileWriter::value_write_column_batch()   // Tablet 列指针 → 连续数组
  └── ValueChunkWriter::write_batch()      // 管理 Page 边界分拆
        └── ValuePageWriter::write_batch() // 全量通过 or null 散列
              └── encoder->encode_batch()  // 编码器批量入口 ← SIMD 发力点

读取路径同理,read_batch_int32() / read_batch_int64() 以连续数组为输出目标。

批量化的意义:逐值接口(write(t, v))在编码器内部无法形成连续数组,SIMD 无从下手;批量接口将整列数据以裸指针传入,使编码/解码核心可直接操作向量寄存器宽度对齐的内存块。

5.2 SIMDe 跨平台方案

  • SIMDe(SIMD Everywhere)库:透明模拟层,同一份代码在 x86/ARM/无 SIMD 平台上均可编译
  • 三级回退:AVX2 原生 → NEON 模拟 → 标量回退
  • 条件编译:ENABLE_SIMD 关闭后完全不引入 SIMD 头文件依赖
  • ARM 时间过滤路径可直接使用原生 NEON(__ARM_NEON 宏检测),绕过 SIMDe 模拟层

5.3 写入路径的 SIMD 加速

TS2DIFF 编码的三个阶段均可向量化,批量化接口使这三步直接操作连续数组:

Step 1:差分计算(待实现)

$$\delta_i = v_i - v_{i-1}, \quad i = 1, \ldots, n-1$$

相邻差是典型的向量减法:加载 $[v_1, v_2, \ldots, v_n]$ 和 $[v_0, v_1, \ldots, v_{n-1}]$,一次 _mm256_sub_epi64 得到 4 个差分值。

Step 2:Rebase(已实现)

$$\delta'i = \delta_i - \delta{\min}$$

  • INT32:SSE4.2 _mm_sub_epi32(4 值/次)
  • INT64:AVX2 _mm256_sub_epi64(4 值/次)

Step 3:位打包(待实现)

将 $\delta'_i$ 以 $w$ 位宽紧密打包写入字节流。SIMD 实现通过 scatter + blend 指令将多个值的位域压缩到连续字节,与解码路径的 gather 操作对称。

5.4 读取路径的 SIMD 加速

5.4.1 TS2DIFF 批量解码

TS2DIFF 解码是编码的逆过程,批量解码以 4/8 值为步长:

  • INT32(128-bit):Gather → 字节序转换 → 位对齐提取 → 加偏移 → 前缀和还原 → 加基值
  • INT64(256-bit):同 lane 前缀和 + 跨 lane 标量补偿(AVX2 lane 隔离限制)
  • 尾部不足一个向量宽度的值回退到标量路径

5.4.2 编码感知的 Block 级谓词下推

直接从 TS2DIFF 块头推断值域,无需解码:

  • peek_next_block_range():读取 16-24 字节块头,计算 $[v_{\min}, v_{\max}]$
  • skip_peeked_block():直接跳过位打包数据(advance_read_pos
  • 与 Parquet 对比:TsFile 独有的第三级过滤(Chunk → Page → Block),Parquet 仅支持两级

5.4.3 时间谓词的向量化过滤

satisfy_batch_time() 对连续时间戳数组应用谓词,输出布尔掩码:

  • x86 AVX2:_mm256_cmpgt_epi64 + movemask_pd,4 值/次
  • ARM NEON:vcgtq_s64,2 值/次(原生,不经 SIMDe)
  • 覆盖全部时间谓词:TimeGtTimeGtEqTimeLtTimeLtEqTimeEqTimeNotEqTimeBetween
  • 不做值过滤:只关注时间维度的过滤,与 TsFile 的时序语义一致

5.5 全链路视图

写入路径                              读取路径
──────────────────────────────────────────────────────
encode_batch(values[], n)            read_batch(out[], n)
     │                                      │
     ▼                                      ▼
差分计算 δ[]    [SIMD 待实现]        位解包 + 字节序  [SIMD 已实现]
     │                                      │
     ▼                                      ▼
Rebase δ'[]     [SIMD 已实现]        前缀和还原       [SIMD 已实现]
     │                                      │
     ▼                                      ▼
位打包写入      [SIMD 待实现]        加基值输出       [SIMD 已实现]
     │
     ▼                              satisfy_batch_time(times[], mask[])
Page ByteStream                          [SIMD 已实现,仅时间过滤]

5.6 实验评估

对比 ENABLE_SIMD=ON vs OFF,分别测写入编码吞吐和读取解码 + 过滤吞吐:

实验路径变量指标
编码吞吐写入SIMD ON/OFF × 数据类型M rows/s
解码吞吐读取SIMD ON/OFF × 数据类型M rows/s
时间过滤吞吐读取SIMD ON/OFF × 选择率M rows/s
跨平台对比读写x86 AVX2 vs ARM NEON vs 标量加速比

5.7 本章小结


第 6 章 面向 AI 负载的优化成果应用

定位:本章是前三章优化成果的应用出口。第 3-5 章分别在内存、CPU、指令集三个维度完成了底层优化;本章展示这些优化如何通过 Arrow C Data Interface 以最小开销透传到 AI/ML 生态,使底层的列式高效读取、多线程解码、SIMD 加速最终呈现在 Python DataFrame 接口上。 状态:核心实现已确认——Arrow C Data Interface 双向桥接 + Python DataFrame API,已通过测试

6.1 AI 工作负载的时序数据访问特征

  • 与传统监控读取的差异:随机时间窗口 vs 连续扫描,小批量/多 epoch vs 大批量单次
  • 列裁剪收益:特征选择阶段只读部分测点
  • 读放大问题:随机窗口访问触发整 Page 解码,多级过滤体系的缓解效果

6.2 Arrow C Data Interface 桥接架构

AI/ML 生态(pandas、PyTorch、polars 等)普遍以 Apache Arrow 为数据交换格式。TsFile 通过实现 Arrow C Data Interface(而非依赖 pyarrow C++ 库)实现了轻量的跨语言互操作。

C++ TsFile                         Python / pyarrow
─────────────────────────────────────────────────────
TsBlock (列式内存)                  pa.RecordBatch
     │                                   ▲
     │  TsBlockToArrowStruct()           │  pa.RecordBatch._import_from_c(
     ▼                                   │      array_ptr, schema_ptr)
ArrowArray + ArrowSchema  ──────────────┘
(Arrow C Data Interface)
     │
     ▼
pa.RecordBatch → pd.DataFrame

设计选择:Arrow C Data Interface 是纯 C ABI 的标准(ArrowArray / ArrowSchema 结构体),不依赖 Arrow C++ 运行时,与 TsFile 的极简依赖原则一致。

6.3 TsBlock → Arrow 转换(读取路径)

TsBlockToArrowStruct() 将一个 TsBlock 转换为 Arrow struct array:

内存模型

情况操作内存开销
非 null 数值列memcpy Vector ByteBuffer → Arrow buffer1× 数据大小
含 null 数值列scatter 非 null 值 + 分配 null bitmap(位反转)1× 数据大小 + bitmap
字符串列重组为 Arrow offset+data 格式1× 数据大小 + offset 数组
DATE 列YYYYMMDD → days-since-epoch 逐值转换1× int32 数组

“单拷贝”语义:转换过程中每列数据最多经历一次 memcpy,无中间格式。pyarrow 的 _import_from_c() 接受指针所有权,本身是零拷贝。整体链路:1 次 memcpy(C++侧)+ 0 次拷贝(Python 侧)

null bitmap 位语义差异:TsFile 内部 bitmap 以 1=null,Arrow 以 1=valid,转换时逐字节取反(~byte)。

类型映射

TsFile 类型Arrow format string说明
INT32"i"直接映射
INT64"l"直接映射
TIMESTAMP"tsn:"纳秒时间戳
FLOAT"f"直接映射
DOUBLE"g"直接映射
BOOLEAN"b"字节→位打包
STRING/TEXT"u"offset+data 格式
DATE"tdD"YYYYMMDD→天数转换

6.4 Arrow → Tablet 转换(写入路径)

ArrowStructToTablet() 将 Arrow struct array 转换回 Tablet 供写入:

  • 数值列(INT32/INT64/FLOAT/DOUBLE):set_column_values() 直接使用 Arrow buffer 指针,有效避免额外拷贝
  • 字符串列:逐行从 Arrow offset+data 格式提取
  • DATE 列:days-since-epoch → YYYYMMDD 逐值转换
  • 支持可选的 TableSchema 参数:当已注册 schema 时,以 schema 类型为准(优先于 Arrow format 推断)

6.5 Python DataFrame API

基于上述 Arrow 桥接层,Python 侧提供两个核心接口:

读取:to_dataframe()

to_dataframe(
    file_path,
    table_name=None,       # 默认使用文件中第一张表
    column_names=None,     # 列裁剪:None = 全列
    start_time=None,       # 时间范围过滤
    end_time=None,
    max_row_num=None,      # 行数限制(LIMIT 语义)
    as_iterator=False      # True → Iterator[DataFrame],流式读取
)
  • as_iterator=True:分批 yield DataFrame,内存峰值受 batch_size 控制,适合大文件流式处理
  • 列裁剪直达 C++ 读取层,未指定的列不被解码

写入:write_dataframe() / dataframe_to_tsfile()

# 已有 schema 的写入
with TsFileTableWriter(path, schema) as writer:
    writer.write_dataframe(df)

# schema 自动推断写入
dataframe_to_tsfile(df, path, table_name, time_column, tag_column)
  • write_dataframe 支持大小写不敏感的列名匹配
  • dataframe_to_tsfile 从 pandas dtype 自动推断 TsFile 类型

6.6 实验评估

实验变量指标
端到端读取吞吐列数 × batch_sizeM rows/s,与 Parquet+pyarrow 对比
Arrow 转换开销占比行数 × 列数转换耗时 / 总查询耗时
迭代器模式内存峰值文件大小 × batch_size峰值内存 MB
写入吞吐行数 × 列数M rows/s

6.7 本章小结


附录:各章节依赖关系与实验工作量评估

章节依赖现有工作待新增实现待采集实验
第 3 章 内存sec3.md 全部 + PageArena/LRU 代码读侧内存分布分析无(写侧已有)
第 4 章 多线程AlignedChunkReader 并行解码、ThreadPool写侧并行粒度确认写/读并行加速比
第 5 章 SIMDTS2DIFF 解码、时间过滤、rebase值过滤/差分/位打包 SIMD(可选)全链路 ON vs OFF 对比
第 6 章 AI 接口TsBlock 设计、多级过滤、LRU方向待定端到端 benchmark

下一步:逐章讨论,从第 3 章开始确认内容范围和实验设计。