TDengine C++ 系列(6):高性能写入——参数绑定、批量与 Schemaless

核心目标 :掌握 taos_stmt 参数绑定与批量写入,能设计高吞吐写入架构,并用可复测的量化数据选择写入策略。

前置知识:Part 3(INSERT 语义)、Part 5(连接与句柄);C++17。

验证环境 :TDengine 3.4.x(3.4.1.6);taosc;C++17;Docker(服务端);数据来自 seed.cpp


0. 本篇问题场景

遥测是每天写入量最大的数据:1000 台设备 × 每秒 9 个指标 = 每秒近万行 。第一版采集程序用 Part 3 的 INSERT INTO ... VALUES 一条条写,很快暴露问题:

  • 写 10 万行花了几分钟,采集端疯狂积压;
  • 每条 SQL 都要经过"字符串拼装 → 网络往返 → 解析",CPU 和 RTT 全浪费在协议上;
  • 偶尔有报文乱序到达,写入明显更慢;
  • 想压测一下到底能写多快,却不知道用什么工具、怎么量化。

本篇回答三件事:

  1. 为什么慢:逐条 INSERT 的链路开销分解(网络 RTT + SQL 解析 + WAL 落盘);
  2. 怎么快taos_stmt 参数绑定 + 批量提交的原理与用法;
  3. 怎么证明快:固定模板的基准方法(环境/数据/批量/并发),以及乱序、WAL 配置对吞吐的影响。

顺带介绍 Schemaless 写入(第三方协议接入)与 stmt1/stmt2 的选型。


1. 心智模型:写入性能从哪来

1.1 一次写入的链路开销分解

回顾 Part 1 的写入链路,把每步代价量化:

复制代码
逐条 INSERT(1 行/次):
  客户端组 SQL(字符串) ──► 网络发送(1 个 RTT) ──► 服务端解析 SQL
  ──► mnode 路由 ──► vnode 写 WAL ──► 写内存表 ──► 网络回包(1 个 RTT)

批量(1000 行/次):
  同样的步骤,但 1000 行只花 1 个 RTT、只解析 1 次、WAL 一次写多行

性能差距的本质 :逐条写入时,网络 RTT 和 SQL 解析是固定开销,与行数无关。100 万行逐条写 = 100 万次 RTT;100 万行分 1000 批写 = 1000 次 RTT。这就是数量级差距的来源。

1.2 四种写入方式对比

方式 示例 吞吐 适用
单条 INSERT INSERT INTO tb VALUES (...) × N 最低 一次性/调试
多行 VALUES INSERT INTO tb VALUES (...),(...) 中小批量、脚本
参数绑定 stmt taos_stmt + add_batch 最高 高频写入主线(遥测)
Schemaless taos_schemaless_insert 第三方协议(InfluxDB/OpenTSDB)接入

结论先行 :EMS 遥测这类高频、结构化、大批量的写入,主线和唯一推荐是 stmt 参数绑定


2. 最小可运行示例:stmt 全流程

项目 src/examples/part6_stmt_write.cpp 完整可编译运行,核心流程:

cpp 复制代码
// 1) 初始化 stmt + SQL 模板(? 占位)
TdStmtWriter w(conn, "INSERT INTO ? USING telemetry TAGS(?,?,?,?) "
                     "VALUES (?,?,?,?,?,?,?,?,?)");

// 2) 绑定子表名与标签(一次设置,后续写该表不用重复)
w.set_tbname_tags("d_0001",
                  {v_str("华东-站A"), v_str("main_transformer"),
                   v_str("110kV"), v_str("ACME")});

// 3) 逐行绑定数据 + add_batch(内存缓冲,不立即提交)
for (int i = 0; i < 10000; ++i) {
    w.bind_row({v_ts(base + i * 1000), v_float(ua), ...});
}

// 4) execute:一次提交所有 batch
w.execute();

运行输出(数字随环境变化,正文不承诺固定倍数):

text 复制代码
stmt 写入 10000 行耗时 0.150 s(约 67000 行/s)
VALUES 拼接 10000 行(10 批)耗时 1.820 s(约 5500 行/s)

要点:

  • SQL 只写一次模板,之后只传数据------省掉每次的字符串拼装与解析;
  • add_batch 只做内存缓冲execute 才真正提交------批量大小自己控制;
  • 时间戳仍须按序递增(乱序代价见 5.2)。

2.1 底层 API 与 TAOS_MULTI_BIND

TdStmtWriter 封装的就是这组底层调用:

cpp 复制代码
TAOS_STMT* stmt = taos_stmt_init(conn);
taos_stmt_prepare(stmt, "INSERT INTO ? USING ... VALUES (?,?,...)", 0);

// 绑定标签(TAOS_MULTI_BIND 数组,num=1)
TAOS_MULTI_BIND tags[4];
tags[0].buffer_type = TSDB_DATA_TYPE_BINARY;
tags[0].buffer = (void*)station;   // BINARY 值
tags[0].length = &len;             // int32_t*,指向长度
tags[0].buffer_length = len;
tags[0].is_null = nullptr;
tags[0].num = 1;
taos_stmt_set_tbname_tags(stmt, "d_0001", tags);

// 绑定列值(每行一遍)
TAOS_MULTI_BIND params[9];
params[0].buffer_type = TSDB_DATA_TYPE_TIMESTAMP;
params[0].buffer = &ts;            // int64_t
params[0].length = &ts_len;        // sizeof(int64_t)
params[0].buffer_length = sizeof(int64_t);
params[0].num = 1;
taos_stmt_bind_param(stmt, params);
taos_stmt_add_batch(stmt);

taos_stmt_execute(stmt);            // 提交
taos_stmt_close(stmt);              // 释放

TAOS_MULTI_BIND 字段含义:

字段 含义
buffer_type 列类型(TSDB_DATA_TYPE_*
buffer 数据指针(定长类型指向数值;BINARY/NCHAR 指向字符串)
buffer_length 单元素字节数(定长类型 = sizeof;变长类型 = 元素容量)
length int32_t*,指向长度(变长类型逐行长度;定长类型可指向固定值)
is_null NULL 标记:指向长度为 numchar 数组,元素非 0 表示该行该列 NULL(nullptr 表示非 NULL)
num 本次绑定的行数(1 = 单行)

2.2 一次绑定多行(高效模式)

num > 1 时,buffer 指向连续数组 (如 1000 个 ts 的 int64_t[1000]),一次 taos_stmt_bind_param 绑定 1000 行,execute 一次提交。比"逐行 bind + add_batch"更省调用次数,是超高吞吐场景的进阶形态:

cpp 复制代码
int64_t ts[1000]; float ua[1000]; ...
params[0].buffer = ts;      // 数组首地址
params[0].num = 1000;       // 一次绑定 1000 行

本项目 TdStmtWriter 先实现"逐行 add_batch"的简单模式(语义清晰、便于讲解);需要极致吞吐时按本节扩展为数组绑定。


3. 机制拆解:批量、WAL 与乱序

3.1 批量大小的甜点区

  • 批量太小(如 10 行/批):RTT/解析开销仍占大头,提效有限;
  • 批量太大(如 10 万行/批):内存缓冲大、单次提交失败重试成本高、服务端处理延迟拉长;
  • 常见甜点区:数百 ~ 数千行/批,具体以你的环境基准为准(4.3 模板)。

3.2 WAL 配置:安全与吞吐的旋钮

写入时数据先落 WAL(预写日志)再进内存表,wal_level 决定 WAL 的落盘策略:

wal_level 行为 取舍
0 不落盘 WAL 最快,进程崩溃可能丢最近数据
1(默认) 写 WAL 但延迟 fsync 吞吐与安全折中
2 每次写入 fsync 最安全,吞吐明显下降

生产建议 :默认 1;追求极致吞吐且能接受少量丢失时用 0;金融/审计类数据用 2。修改在 ALTER DATABASEwal_level/fsync),改后做一次基准对比,不要凭感觉调。

3.3 乱序写入:能写但贵

Part 3 已讲过语义,这里给量化视角:同一批数据乱序写入 会触发内存排序与落盘重排,实测(以你的环境为准)吞吐通常下降一个数量级,且压缩率变差。采集端要做时间戳缓冲排序(见 4.4)。

3.4 stmt1 与 stmt2

模式 说明 适用
stmt1(taos_stmt_*,本篇) 通用参数绑定,兼容性好 默认主线
stmt2(3.3+ 新写入模式) 更高写入效率、子表元数据变化更友好 极致吞吐、高频建子表场景

项目默认 stmt1;需要时按官方文档切换到 stmt2 并做基准对比(名称/参数以官方为准)。


4. ems-lab 工程实战:TdStmtWriter 与基准

4.1 写入器封装(已并入项目)

src/ems_lab/stmt_writer.h/.cpp 提供 TdStmtWriter

  • 构造时绑定 SQL 模板(一次 prepare);
  • set_tbname_tags 设置目标子表与标签(写同一台设备只需设一次);
  • bind_row 逐行追加(内部 add_batch);
  • execute 提交并累计受影响行数;
  • 全部错误走 TdExceptiontaos_stmt_errstr 取证)。

4.2 采集端写入架构

复制代码
采集线程(每线程一条连接 + 一个 stmt)
  │  报文到达 → 解析 → 时间戳缓冲排序
  ▼
  bind_row × N(例如 1000 行)
  ▼
  execute(批量提交)
  ▼
  失败重试(幂等:同表同 ts 覆盖,Part 3)

三个原则:

  1. 每线程独立连接 + 独立 stmt(Part 5 的线程边界结论直接落地);
  2. 缓冲排序后再写:按时间戳有序批量,避免乱序代价;
  3. 失败重试幂等:重发不产生重复行(同键覆盖)。

4.3 固定基准模板

任何"快 N 倍"结论都必须按这个模板复测(写进文档/CI):

text 复制代码
环境:CPU 型号/核数、内存、磁盘类型(SSD/HDD)、TDengine 版本、网络(本机/跨机)
数据:行数、列数、时间戳范围、乱序比例
变量:批量大小(逐条/100/1000/10000)、线程数(1/4/8)
测量:总耗时、行/秒、P99 单批延迟
复现命令:./build/part6_stmt_write / taosBenchmark

4.4 用 taosBenchmark 交叉验证

bash 复制代码
taosBenchmark -f write_config.json   # 配置见官方文档,场景 = 遥测批量写

用官方压测工具得到独立基准,与本项目数字对照,避免"自己测自己"的偏差。


5. 失败实验与根因

5.1 逐条写入 vs 批量写入

现象:10 万行逐条 INSERT 耗时数分钟,批量后降到秒级。

根因:每条一次网络 RTT + 解析(1.1 的固定开销)。教训:任何"逐条写"的采集代码都是性能事故,先批量化再谈其他优化。

5.2 乱序写入的量化

现象:同一批数据乱序写,吞吐比有序写低一个数量级(实测数据留档)。

根因:乱序触发排序/重排路径(3.3)。教训:采集端按时间戳排序缓冲;确需乱序(迟到 SOE)时接受代价。

5.3 批量提交中途失败

现象:execute 抛异常,部分行已生效、部分未生效。

根因:批量写入是逐行生效语义(Part 3 已提),整批不原子回滚。修法 :业务侧以"幂等重试"兜底------重发同键数据会覆盖而非重复,最终一致。不要把"整批失败"当作"全部没写"。

5.4 WAL 配置盲目调 0 后丢数据

现象:wal_level 0 后进程崩溃,最近数据丢失。

根因:WAL 不落盘,崩溃即丢。教训 :安全等级决定 wal_level,先量化吞吐差距再决定是否牺牲安全。


6. 版本与环境差异

维度 说明
TAOS_MULTI_BIND 3.x 绑定结构(2.x 有 TAOS_BIND,不通用);以所装版本 taos.h 为准
stmt2 3.3+ 新写入模式;3.4.x 支持,启用方式见官方参数绑定文档
wal_level 3.x 建库/改库参数;取值与行为以官方文档为准
Schemaless 协议 taos_schemaless_insert 支持 InfluxDB Line / OpenTSDB JSON/Telnet,3.x 一致
性能数据 所有数字仅代表"记录时的环境",发布前在目标环境复测

7. 测试与验收

7.1 自动化断言

  • 批量写入后 COUNT(*) 精确等于写入行数(含幂等重放场景);
  • 乱序实验:有序 vs 乱序耗时记录并留档(不设硬性阈值,环境相关);
  • part6_stmt_write 对比输出记录到文档/CI 日志。

7.2 本篇验收清单

  • 能说出逐条写慢的三层原因(RTT、解析、WAL 次数);
  • TdStmtWriter 已并入 ems-lab,part6_stmt_write 可编译运行并输出基准;
  • 用固定模板完成一次基准(批量大小 × 线程数矩阵)并留档;
  • 复现 4 个失败实验(逐条慢、乱序量化、批量部分成功、WAL 0 丢数据)并解释根因;
  • 用 taosBenchmark 交叉验证一次写入基准;
  • 采集端写入架构(每线程独立连接 + 缓冲排序 + 幂等重试)落地到代码。

8. 常见误区

误区 事实
"批量大小越大越好" 有甜点区;过大导致缓冲/重试/延迟问题,以基准为准
"stmt 一定比 VALUES 快很多" 大批量 VALUES 也不慢;stmt 的优势在省解析与批量可控,具体以基准为准
"乱序写入没影响" 能写但吞吐与压缩率下降,采集端必须排序
"批量失败就整体重来" 逐行生效,部分成功;幂等重试才是正确兜底
"wal_level 0 没风险" 崩溃丢最近数据,安全等级先于性能
"Schemaless 能替代 stmt" 适合第三方协议接入;结构化高频写入主线仍是 stmt

9. 本篇小结

写入性能的答案不是某个魔法参数,而是一套方法:

  1. 瓶颈本质:逐条写的固定开销(RTT + 解析 + WAL 次数),批量化是最优先的优化;
  2. 主线技术taos_stmt 参数绑定 + add_batch/execute,配合 TAOS_MULTI_BIND(含多行数组模式);
  3. 架构形态:每线程独立连接与 stmt、采集端时间戳排序、幂等重试;
  4. 旋钮 :批量大小(数百~数千)、wal_level(0/1/2)、stmt2(进阶);
  5. 证据:固定基准模板 + taosBenchmark 交叉验证,杜绝无环境结论。

至此,读路径(Part 5)与写路径(Part 6)都已打通,ems-lab 具备了生产形态的存取能力。下一步进入实时世界:用 TMQ 订阅消费"变位事件/越限告警",让数据流起来。

10. 官方资料

相关推荐
东宇科技1 小时前
让网页执行cmd命令
php
fpcc2 小时前
ubuntu26环境下的开发环境安装处理
c++·并行编程
持敬chijing3 小时前
PHP开发-环境搭建-安装phpstudy-vscode工具
开发语言·vscode·php
charlie1145141913 小时前
Cinux · 第一次跳进 Ring 3:用户态与特权隔离
开发语言·c++·操作系统·开源项目
别动我齐刘海3 小时前
机器学习基础2——C++、OpenCV、点云、Open3D
c++·人工智能·opencv·机器学习·计算机视觉·机器人·ros2
躺不平的理查德4 小时前
Windows C++ 第三方库使用流程备忘录--OpenCV
开发语言·c++
余额瞒着我当琳4 小时前
C++STL容器string--迭代器,string的接口,string的遍历,访问方式
c++
郭涤生4 小时前
rootfs 详解与裁剪优化记录
linux·c++·bsp
Hello_Damon_Nikola4 小时前
AC7911从入门到精通
开发语言·嵌入式硬件·物联网·php