每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用

Background

  • 上年6月份写过一篇关于Machbase时许数据库的简单介绍 【时序数据库Machbase】。
  • 但之前只是简单介绍了下,今天我们详细介绍下,主要是Machbase中针对存储传感器监测数据而设计的Tag table的基本使用,并在本地单机环境简单测试了一下数据的写入性能,写入的数据和之前测试中【Influxdb和TDengine的写入性能测试(Java)】使用的一样,一个传感器带三个指标数据。最终测试结果为【125万条/秒】【21M/百万条】。这次测试的服务器和之前测试Influxdb和TDengine是在同一个服务器上。
  • 大家应该都知道我有个elephant服务器了吧 ,这里给出它的配置,如下图,请放下你的赛博武器

每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第1张图片

1、国际惯例,先放上测试结果

  • 测试源码较长,放在文末哈。
  • 其实,官方提供了一个参数 tag_partition_count(默认值是4)来控制cpu和内存的使用以提供不同的处理性能。我测试用的默认配置值4。
  • 后面我也测试了下把 tag_partition_count值改成1时,内存和cup的使用都降低了,处理性能也有所下降,如第二个图所示。当然我们也可以增大这个值来提高性能,这里我就不测试了,服务器没资源了哈。
  • tag_partition_count=4
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第2张图片
  • tag_partition_count=1
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第3张图片
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第4张图片

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目录是日志存储目录)
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第5张图片
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第6张图片
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第7张图片
  • 数据压缩和Influxdb差不多,都是【21M/百万条】
    每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第8张图片
  • 不同字段及字段存储不同数据所占用的磁盘空间
  • 此处统计的是数据存储目录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/,从服务器上下载下来。

每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第9张图片

4、安装依赖

开发工具我用的idea,把下载的驱动包安装到依赖里。我这里是上传到了maven私服,然后引的maven依赖。

        
            com.machbase
            mach-jdbc
            6.5.8
        

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 中元素的类型 [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 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年首发,但相关经验博客几乎没有,以上都是参考的官方文档,若有错漏之处,敬请斧正哈。

每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第10张图片

每秒125万条写入速度-时序数据库Machbase中的Tag table的基本使用_第11张图片

你可能感兴趣的:(Java,大数据,java,machbase)