核心目标 :掌握
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 全浪费在协议上;
- 偶尔有报文乱序到达,写入明显更慢;
- 想压测一下到底能写多快,却不知道用什么工具、怎么量化。
本篇回答三件事:
- 为什么慢:逐条 INSERT 的链路开销分解(网络 RTT + SQL 解析 + WAL 落盘);
- 怎么快 :
taos_stmt参数绑定 + 批量提交的原理与用法; - 怎么证明快:固定模板的基准方法(环境/数据/批量/并发),以及乱序、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 标记:指向长度为 num 的 char 数组,元素非 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 DATABASE(wal_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提交并累计受影响行数;- 全部错误走
TdException(taos_stmt_errstr取证)。
4.2 采集端写入架构
采集线程(每线程一条连接 + 一个 stmt)
│ 报文到达 → 解析 → 时间戳缓冲排序
▼
bind_row × N(例如 1000 行)
▼
execute(批量提交)
▼
失败重试(幂等:同表同 ts 覆盖,Part 3)
三个原则:
- 每线程独立连接 + 独立 stmt(Part 5 的线程边界结论直接落地);
- 缓冲排序后再写:按时间戳有序批量,避免乱序代价;
- 失败重试幂等:重发不产生重复行(同键覆盖)。
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. 本篇小结
写入性能的答案不是某个魔法参数,而是一套方法:
- 瓶颈本质:逐条写的固定开销(RTT + 解析 + WAL 次数),批量化是最优先的优化;
- 主线技术 :
taos_stmt参数绑定 + add_batch/execute,配合TAOS_MULTI_BIND(含多行数组模式); - 架构形态:每线程独立连接与 stmt、采集端时间戳排序、幂等重试;
- 旋钮 :批量大小(数百~数千)、
wal_level(0/1/2)、stmt2(进阶); - 证据:固定基准模板 + taosBenchmark 交叉验证,杜绝无环境结论。
至此,读路径(Part 5)与写路径(Part 6)都已打通,ems-lab 具备了生产形态的存取能力。下一步进入实时世界:用 TMQ 订阅消费"变位事件/越限告警",让数据流起来。