Background
- 上年6月份写过一篇关于Machbase时许数据库的简单介绍 【时序数据库Machbase】。
- 但之前只是简单介绍了下,今天我们详细介绍下,主要是Machbase中针对存储传感器监测数据而设计的Tag table的基本使用,并在本地单机环境简单测试了一下数据的写入性能,写入的数据和之前测试中【Influxdb和TDengine的写入性能测试(Java)】使用的一样,一个传感器带三个指标数据。最终测试结果为【125万条/秒】【21M/百万条】。这次测试的服务器和之前测试Influxdb和TDengine是在同一个服务器上。
- 大家应该都知道我有个elephant服务器了吧 😎,这里给出它的配置,如下图,请放下你的赛博武器 🙏
1、国际惯例,先放上测试结果
- 测试源码较长,放在文末哈。
- 其实,官方提供了一个参数 tag_partition_count(默认值是4)来控制cpu和内存的使用以提供不同的处理性能。我测试用的默认配置值4。
- 后面我也测试了下把 tag_partition_count值改成1时,内存和cup的使用都降低了,处理性能也有所下降,如第二个图所示。当然我们也可以增大这个值来提高性能,这里我就不测试了,服务器没资源了哈。
- tag_partition_count=4
- tag_partition_count=1
2、Machbase Tag Table
1️⃣ 针对不同的业务场景,Machbase分了四个表类型【Tag, Log, Volatile, Lookup】,其中Tag table是用来处理传感器监测的时许数据,也是我们这里着重介绍的;Log table逻辑概念类似关系型数据库,应用场景适合业务系统日志、用户数据等历史时许数据的处理分析;Volatile table是全内存表,数据库系统关闭后所有数据会丢失,应该场景适合内存计算中间变量的存储;Lookup table看官方介绍是用来持久化存储主节点信息的,这里没深入研究;其中前两个存储数据大小受磁盘大小限制,后两个受内存大小限制。
2️⃣Tag table 的一些特点:
- 没有分库分表的概念(有tablespace的概念,但只是针对Log table使用的),只有一个总表,且表名固定为tag;
- tag表在使用前需要设计好表结构并预先创建,表的字段、字段个数及类型即固定了,所以如果存储指标个数不同的多个类型的传感器,则存在冗余字段(底层数据存储做的有优化,虽有影响但不大);
- 没有主键概念。完全相同的一条数据可以重复存储(也没有版本号的概念);
- 不支持按tag和时间段删除数据。数据删除只支持全删和单时间节点全量删除(删除某一时间节点前的所有数据);
- 官方文档提供了三种数据写入方法。1️⃣ JDBC; 2️⃣ RestApi; 3️⃣ Tag table RestApi;
- 数据写入的所有方法中,推荐第一种方法JDBC,第二种方法不支持批量写入,速度每秒几百条,第三种支持批量,写入速度每秒2万条,但没有权限认证,是基本Web server的。
- 为了测试需要首先创建tag表
create tagdata table tag(targetId varchar(20) primary key, datatime datetime basetime, v1 double summarized, v2 float, v3 float) metadata(projectId short, targetTypeId short);
- 创建好后会生成一堆表,并且内存占用从600M左右直接飙到2.6个G,磁盘占用从65M直接飙到2.7个G,即在我们使用之前预先做了很多工作,比如ROLLUP表是为页面的tag view分析准备的。(dbs目录是数据存储目录,trc目录是日志存储目录)
- 数据压缩和Influxdb差不多,都是【21M/百万条】
- 不同字段及字段存储不同数据所占用的磁盘空间
- 此处统计的是数据存储目录
dbs
的大小。- 测试数据量5百万。
- 表中的tag有三个,targetId,projectId,targetTypeId。
- 由下表的测试结果可以看出,虽然有字段冗余,但由于底层数据存储有优化,所有磁盘空间影响不大。
字段个数及存储的数据 | 5百万条数据占用磁盘空间 |
---|---|
完全相同的一条数据且只有一个字段 | 16.09M |
只有一个字段且数值固定 | 62.67M |
三个字段且数值都是随机 | 94.57M |
三个字段且后两个数值为null | 66.38M |
十个字段且数值都是随机 | 202.69M |
十个字段且后九个数值为null | 74.27M |
十个字段且后九个数值固定 | 74.77 |
3、下载jdbc驱动包
不用网上下载,就在安装目录lib下
/home/software/machbase-fog-6.5.8/lib/
,从服务器上下载下来。
4、安装依赖
开发工具我用的idea,把下载的驱动包安装到依赖里。我这里是上传到了maven私服,然后引的maven依赖。
<dependency>
<groupId>com.machbase</groupId>
<artifactId>mach-jdbc</artifactId>
<version>6.5.8</version>
</dependency>
5、最后附上写入测试源码
测试方法是基于第一种方法JDBC。其他一些基本的依赖像lombok,hutool等我就不贴了。
- TargetDataEntity
package com.cloudansys.core.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TargetDataEntity implements Serializable {
private static final long serialVersionUID = -457566433778417297L;
// 测点id
private Integer targetId;
// 项目id
private Integer projectId;
// 类型id
private Integer targetTypeId;
// 数据时间
private Date dataTime;
// 指标数据,按照顺序,1,2,3,...
private List<Double> values;
}
- 测试类
package com.cloudansys.test;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.cloudansys.core.entity.TargetDataEntity;
import com.machbase.jdbc.MachAppendCallback;
import com.machbase.jdbc.MachStatement;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
public class T1 {
public static void main(String[] args) {
// 总共多个测点,每个测点多少万条数据
int count = 50, batch = 10;
int projectId = 100, targetTypeId = 1;
// 准备数据
System.out.println("数据准备中...");
List<TargetDataEntity> entities = new ArrayList<>();
long nowMillis = DateUtil.date().getTime();
for (int i = 1; i <= count; i++) {
long dataTime = nowMillis;
for (int j = 0; j < batch * 10000; j++) {
TargetDataEntity entity = TargetDataEntity.builder()
.targetId(i)
.dataTime(DateUtil.date(dataTime))
.values(ListUtil.toList(ranVal(), ranVal(), ranVal()))
.projectId(projectId)
.targetTypeId(targetTypeId)
.build();
entities.add(entity);
dataTime += 100;
}
}
saveData(entities);
}
/**
* 写入数据
*/
private static void saveData(List<TargetDataEntity> entities) {
Connection conn;
MachStatement stmt;
String table = "tag";
int interval = 100, count = 0;
List<ArrayList<Object>> dataList = transformData(entities);
try {
conn = connect();
stmt = (MachStatement) conn.createStatement();
System.out.println("Mach JDBC 连接成功");
// 先打开append,获取meta信息
ResultSet rs = stmt.executeAppendOpen(table, interval);
ResultSetMetaData metaData = rs.getMetaData();
// 设置错误回调函数
String errMsgTpl = "Append Error : [{} - {}]\n{}\n";
MachAppendCallback mcb = (
aErrNo, aErrMsg, aRowMsg) -> System.out.println(StrUtil.format(errMsgTpl, aErrNo, aErrMsg, aRowMsg));
stmt.executeSetAppendErrorCallback(mcb);
// 开始写入数据
System.out.println("数据写入中...");
long start = System.currentTimeMillis();
for (ArrayList<Object> data : dataList) {
if (stmt.executeAppendData(metaData, data) != 1) {
System.err.println("Error : AppendData error");
}
if ((count++ % 100000) == 0) {
System.out.print(".");
}
}
// 写入结束关闭append
stmt.executeAppendClose();
long end = System.currentTimeMillis();
long cost = (end - start) / 1000;
double speed = NumberUtil.round((double) (count / 10000) / cost, 1).doubleValue();
long successCount = stmt.getAppendSuccessCount();
long failureCount = stmt.getAppendFailureCount();
System.out.println("\n总条数:" + count / 10000 +" 万条");
System.out.println("写入结果 : success = " + successCount + ", failure = " + failureCount);
System.out.println("耗时:" + cost + " 秒");
System.out.println("写入速度: " + speed + " 万条/秒");
} catch (Exception e) {
e.printStackTrace();
}
// 官方文档上有关闭,但我这里关闭会报错
// finally {
// close(stmt);
// close(conn);
// }
}
/**
* 组织数据写入格式
*/
private static List<ArrayList<Object>> transformData(List<TargetDataEntity> entities) {
// 组织数据
System.out.println("数据组织中...");
List<ArrayList<Object>> dataList = new ArrayList<>();
for (TargetDataEntity entity : entities) {
// data 数据添加必须按照顺序:targetId,dataTime,v1,v2,...,projectId,targetTypeId
ArrayList<Object> data = new ArrayList<>();
data.add(entity.getTargetId().toString());
// 时间必须转成纳秒级时间戳
data.add(entity.getDataTime().getTime() * 1000 * 1000);
List<String> values = entity.getValues().stream().map(String::valueOf).collect(Collectors.toList());
data.addAll(values);
data.add(entity.getProjectId().toString());
data.add(entity.getTargetTypeId().toString());
dataList.add(data);
}
return dataList;
}
/**
* 获取连接
*/
private static Connection connect() {
Connection conn = null;
try {
String url = "jdbc:machbase://elephant:5656/mhdb";
Properties props = new Properties();
props.put("user", "sys");
props.put("password", "manager");
Class.forName("com.machbase.jdbc.driver");
conn = DriverManager.getConnection(url, props);
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭资源
*/
private static void close(AutoCloseable obj) {
if (null != obj) {
try {
obj.close();
} catch (Exception e) {
System.out.println(obj + " close failed");
e.printStackTrace();
}
}
}
/**
* 获取随机数
*/
private static double ranVal() {
return NumberUtil.round(Math.random(), 3).doubleValue();
}
}
6、另附RestApi测试源码
- TargetSaveEntity
package com.cloudansys.core.entity;
import lombok.*;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TargetSaveEntity implements Serializable {
private static final long serialVersionUID = -457566433778417297L;
// 数据集合 [[targetId, dataTime, v1, projectId, targetTypeId, v2, v3],[],...]
// List<Object> 中元素的类型 [String, String, double, int, int, double, double]
private List<List<Object>> values;
// 时间格式,默认毫秒级,名字固定的不要改
@Builder.Default
private String date_format = "YYYY-MM-DD HH24:MI:SS.mmm";
}
package com.cloudansys.test;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cloudansys.core.constant.Const;
import com.cloudansys.core.entity.TargetDataEntity;
import com.cloudansys.core.entity.TargetSaveEntity;
import okhttp3.*;
import org.junit.Test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class T2 {
@Test
public void test() throws Exception {
}
@Test
public void test3() throws Exception {
// 总共多个测点,每个测点多少万条数据
int count = 10, batch = 1;
int projectId = 100, targetTypeId = 1;
// 准备数据
System.out.println("数据准备中...");
List<List<String>> sqlList = new ArrayList<>();
String metaSqlTpl = "insert into tag metadata values('{}', {}, {})";
String dataSqlTpl = "insert into tag values ('{}', '{}', {}, {}, {})";
long nowMillis = DateUtil.date().getTime();
for (int i = 1; i <= count; i++) {
long dataTime = nowMillis;
for (int j = 0; j < batch * 10000; j++) {
String nowTime = DateUtil.date(dataTime).toString(Const.FMT_STD_MILLI);
String metaSql = StrUtil.format(metaSqlTpl, i, projectId, targetTypeId);
String dataSql = StrUtil.format(dataSqlTpl, i, nowTime, ranVal(), ranVal(), ranVal());
sqlList.add(ListUtil.toList(metaSql, dataSql));
dataTime += 100;
}
}
// 写入数据
int amount = count * batch;
System.out.println("正在写入数据:" + amount + "万条");
long start = System.currentTimeMillis();
insert(sqlList);
long end = System.currentTimeMillis();
long cost = (end - start) / 1000;
System.out.println("耗时:" + cost + "秒");
System.out.println("写入速度:" + NumberUtil.round((double) amount / cost, 2) + "万条/秒");
}
/**
* 1千万条数据耗时462秒,占用磁盘空间213.5M;
* 【2.2万条/秒】【21.35M/百万条】
*/
@Test
public void test2() throws Exception {
// 总共多个测点,每个测点多少万条数据
int count = 100, batch = 1;
int projectId = 100, targetTypeId = 1;
// 准备数据
System.out.println("数据准备中...");
List<TargetSaveEntity> dataList = new ArrayList<>();
long nowMillis = DateUtil.date().getTime();
for (int i = 1; i <= count; i++) {
long dataTime = nowMillis;
List<List<Object>> values = new ArrayList<>();
for (int j = 0; j < batch * 10000; j++) {
String nowTime = DateUtil.date(dataTime).toString(Const.FMT_STD_MILLI);
List<Object> val = ListUtil.toList(
String.valueOf(i), nowTime, ranVal(), ranVal(), ranVal(), projectId, targetTypeId);
values.add(val);
dataTime += 100;
}
TargetSaveEntity saveEntity = TargetSaveEntity.builder()
.values(values)
.build();
dataList.add(saveEntity);
}
// 写入数据
int amount = count * batch;
System.out.println("正在写入数据:" + amount + "万条");
long start = System.currentTimeMillis();
batchInsert(dataList);
long end = System.currentTimeMillis();
long cost = (end - start) / 1000;
System.out.println("耗时:" + cost + "秒");
System.out.println("写入速度:" + NumberUtil.round((double) amount / cost, 2) + "万条/秒");
}
@Test
public void test1() {
String sql = "insert into tag values ('1', '2011-04-20 11:39:46.983', 0.35, 0.086, 0.472);";
// String sql = "select * from tag where projectId = 100";
List<TargetDataEntity> res = execMachSql(sql);
}
/**
* 【RestApi】
* 批量插入数据
*/
private void insert(List<List<String>> sqlList) {
String machUrl = "http://elephant:5657/machbase";
String authBase64Str = "Basic c3lzQDEyNy4wLjAuMTpjbG91ZGFuc3lz";
String mediaType = "application/x-www-form-urlencoded";
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(3000000);
dispatcher.setMaxRequestsPerHost(1000000);
for (List<String> sqls : sqlList) {
OkHttpClient client = new OkHttpClient().newBuilder()
// .connectTimeout(100, TimeUnit.SECONDS)
// .writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.dispatcher(dispatcher)
.build();
for (String sql : sqls) {
String body = "q=" + sql;
RequestBody requestBody = RequestBody.create(MediaType.parse(mediaType), body);
Request request = new Request.Builder()
.url(machUrl)
.method("POST", requestBody)
.addHeader("Content-Type", mediaType)
.addHeader("Authorization", authBase64Str)
.build();
try {
Response response = client.newCall(request).execute();
if (null != response.body()) {
System.out.println(response.body().string());
}
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 【Tag Table RestApi】
* 批量插入数据
*/
private void batchInsert(List<TargetSaveEntity> dataList) {
String machUrl = "http://elephant:5001/machiot-rest-api/";
String mediaType = "application/json";
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(3000000);
dispatcher.setMaxRequestsPerHost(1000000);
for (TargetSaveEntity data : dataList) {
OkHttpClient client = new OkHttpClient().newBuilder()
// .connectTimeout(100, TimeUnit.SECONDS)
// .writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(100, TimeUnit.SECONDS)
.dispatcher(dispatcher)
.build();
String jsonBody = JSONUtil.toJsonStr(data);
// System.out.println("jsonBody: " + jsonBody);
RequestBody requestBody = RequestBody.create(MediaType.parse(mediaType), jsonBody);
Request request = new Request.Builder()
.url(machUrl)
.method("POST", requestBody)
.addHeader("Content-Type", mediaType)
.build();
try {
Response response = client.newCall(request).execute();
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 执行 machbase sql
*/
private List<TargetDataEntity> execMachSql(String sql) {
List<TargetDataEntity> res = new ArrayList<>();
String machUrl = "http://elephant:5657/machbase";
String user = "sys";
String pwd = "cloudansys";
String authBase64Str = "Basic " + getAuthBase64Str("elephant", user, pwd);
String mediaType = "application/x-www-form-urlencoded";
String body = "q=" + sql;
OkHttpClient client = new OkHttpClient().newBuilder().build();
RequestBody requestBody = RequestBody.create(MediaType.parse(mediaType), body);
Request request = new Request.Builder()
.url(machUrl)
.method("POST", requestBody)
.addHeader("Authorization", authBase64Str)
.addHeader("Content-Type", mediaType)
.build();
Response response;
try {
response = client.newCall(request).execute();
if (null != response.body()) {
String resBody = response.body().string();
System.out.println(resBody);
JSONObject jsonObject = JSONUtil.parseObj(resBody);
int errorCode = Integer.parseInt(jsonObject.get("error_code").toString());
if (errorCode == 0) {
JSONArray data = JSONUtil.parseArray(jsonObject.getStr("data"));
JSONArray columns = JSONUtil.parseArray(jsonObject.getStr("columns"));
if (data.size() != 0) {
List<Map<String, Object>> cols = new ArrayList<>();
System.out.println("---------------------" + resBody.length());
}
// for (Object obj : columns) {
// Map<String, Object> colMap = new HashMap<>();
// JSONObject parseObj = JSONUtil.parseObj(obj);
// String colName = parseObj.get("name").toString();
// colMap.put(colName, null);
// }
// for (Object obj : data) {
// JSONObject parseObj = JSONUtil.parseObj(obj);
// cols.replaceAll((c, v) -> parseObj.get(c));
// }
// // 把查询结果封装成对象
// for (String col : cols.keySet()) {
// TargetDataEntity targetDataEntity = TargetDataEntity.builder()
// .targetId()
// .build();
// res.add(targetDataEntity);
// }
}
}
} catch (IOException e) {
e.printStackTrace();
}
return res;
}
/**
* create Basic Base64String for authorize
*/
public String getAuthBase64Str(String ip, String user, String pwd) {
String urlTpl = "{}@{}:{}";
String machRestUrl = StrUtil.format(urlTpl, user, ip, pwd);
return Base64.encode(machRestUrl);
}
/**
* 获取随机数
*/
private double ranVal() {
return NumberUtil.round(Math.random(), 3).doubleValue();
}
}
7、后记
由于Machbase不是开源的,虽然和Influxdb都是13年首发,但相关经验博客几乎没有,以上都是参考的官方文档,若有错漏之处,敬请斧正哈。