IoTDB当前支持的客户端
哪种插入方式的效率更高
IoTDB为当前主流语言基本都提供了对应的native api,有一些语言的客户端放在了Apache IoTDB的主仓库下,如Java, C/C++, Python;另一些由于各种语言的依赖管理不同,放在了社区贡献者个人的仓库下,如Golang, C#以及Rust。
因为IoTDB本身是用Java编写,所以Java的客户端更新和维护的较为完善。因为IoTDB提供了一种类sql的查询语言,所以对于Java客户端,不仅提供了native api,还提供了JDBC接口,方便之前熟悉使用JDBC接口的客户迁移。
由于各个客户端native api之间的实现差别不大,所以接下来我们以Java的native api作为native api的代表,与JDBC接口进行写入性能的对比实验。
实验数据建模
关于IoTDB具体的建模方式,在下面这篇文章里有详细介绍,我就不过多赘述了
本次实验的数据模型采用官网的风电场物联网场景的数据模型作为示例:
root.ln.wf01.wt01.status
root.ln
在IoTDB中是存储组的名字,类似于关系型数据库中db的概念,wf01.wt01
是业务场景里的设备名字,最后status
是设备上的传感器名字。一个设备上通常会有多个传感器进行数据采集,所以上述的wf01.wt01
设备可能还有temperature
, speed
等传感器。
实验代码
完整代码我已经放在Github上了: iotdb-insert-experiment
Java Native API(session接口)插入代码
/**
* 使用Session.insertTablet接口插入某一个设备的数据
*/
private static void insertTablet(Session session) throws IoTDBConnectionException, StatementExecutionException {
/*
* 一个Tablet例子:
* deviceID: root.ln.wf01.wt01
* time status, temperature, speed
* 1 true 1.0 1
* 2 false 2.0 2
* 3 true 3.0 3
*/
// 设置设备名字,设备下面的传感器名字,各个传感器的类型
List<MeasurementSchema> schemaList = new ArrayList<>();
schemaList.add(new MeasurementSchema("status", TSDataType.BOOLEAN));
schemaList.add(new MeasurementSchema("temperature", TSDataType.DOUBLE));
schemaList.add(new MeasurementSchema("speed", TSDataType.INT64));
Tablet tablet = new Tablet("root.ln.wf01.wt01", schemaList, BATCH_INSERT_SIZE);
// 以当前时间戳作为插入的起始时间戳
long timestamp = System.currentTimeMillis();
for (long row = 0; row < TOTAL_INSERT_ROW_COUNT; row++) {
int rowIndex = tablet.rowSize++;
tablet.addTimestamp(rowIndex, timestamp);
// 随机生成数据
tablet.addValue("status", rowIndex, (row & 1) == 0);
tablet.addValue("temperature", rowIndex, (double) row);
tablet.addValue("speed", rowIndex, row);
if (tablet.rowSize == tablet.getMaxRowNumber()) {
session.insertTablet(tablet);
tablet.reset();
System.out.println("已经插入了:" + (row + 1) + "行数据");
}
timestamp++;
}
// 插入剩余不足 BATCH_INSERT_SIZE的数据
if (tablet.rowSize != 0) {
session.insertTablet(tablet);
tablet.reset();
}
}
JDBC插入代码
private static void jdbcInsert(Statement statement) throws SQLException {
// 设置设备名字,设备下面的传感器名字,各个传感器的类型
statement.execute("SET STORAGE GROUP TO root.sg1");
statement.execute(
"CREATE TIMESERIES root.ln.wf01.wt01.status WITH DATATYPE=BOOLEAN, ENCODING=RLE, COMPRESSOR=SNAPPY");
statement.execute(
"CREATE TIMESERIES root.ln.wf01.wt01.temperature WITH DATATYPE=DOUBLE, ENCODING=RLE, COMPRESSOR=SNAPPY");
statement.execute(
"CREATE TIMESERIES root.ln.wf01.wt01.speed WITH DATATYPE=INT64, ENCODING=RLE, COMPRESSOR=SNAPPY");
// 以当前时间戳作为插入的起始时间戳
long timestamp = System.currentTimeMillis();
int row = 0;
while (row < TOTAL_INSERT_ROW_COUNT) {
row++;
statement.addBatch(prepareInsertStatment(timestamp + row, row));
if (row % BATCH_INSERT_SIZE == 0) {
statement.executeBatch();
statement.clearBatch();
System.out.println("已经插入了:" + row + "行数据");
}
}
// 插入剩余不足 BATCH_INSERT_SIZE的数据
if (row != 0 && (row % BATCH_INSERT_SIZE) != 0) {
statement.executeBatch();
statement.clearBatch();
}
}
private static String prepareInsertStatment(long time, long row) {
return "insert into root.ln.wf01.wt01(timestamp, status, temperature, speed) values("
+ time
+ ","
+ ((row & 1) == 0)
+ ","
+ ((double) row)
+ ","
+ row
+ ")";
}
实验环境
- 实验机器: MacBook Pro (14-inch, 2021)
- 操作系统: macOS Monterey Version 12.1 (21C52)
- CPU: Apple M1 Pro 10核(8个大核+2个小核)
- 内存: 16GB
- IoTDB版本:0.12.4
- IoTDB配置:默认配置,未做任何修改
实验数据量
- 1个device: root.ln.wf01.wt01
- device下三个传感器
- status: boolean类型
- temperature: double类型
- speed: int64类型
- 总共插入10,000,000行,使用批量插入的方式,每隔1,000行插入一次
实验结果
Session insert 10000000 rows cost: 73551ms.
JDBC insert 10000000 rows cost: 123820ms.
Java Native API(session接口)
插入的方式比JDBC
插入的方式快了68%
实验结论
从上面的实验结果可以看出来,Java Native API
的插入效率要比JDBC
高出很多,所以在选择插入接口时,我们更加推荐用户选择Java Native API
,这也是为什么官网文档的应用编程接口中,JDBC
被标注了不推荐的原因。
其实之所以慢的原因也很简单
- 通过JDBC接口发到iotdb server端的是一条一条的原始sql,server首先需要对这些sql进行解析,这一步就很耗时
- 其次,即使在JDBC客户端这边是将多条插入语句组装成一个Batch发送到server,server这边的处理逻辑依然是一条一条的执行,每一条插入都会拿一次写锁;而对于Native API来说,一次Bacth的插入,在server处只会拿一次写锁,大大减少了拿锁的次数,如果存在读写或写写并发的话,Native API能减少锁冲突的可能性,所以在存在大量并发读写的情况下,两者的性能差异将被进一步放大