智能硬件上报数据存储选型以及架构设计

1. 背景

我们的物联网智能硬件项目,通常会有各种硬件模组上报数据,这些数据需要存储起来便于分析查询。我们一般有如下需求:

  • 硬件模组持续不断高频率上报大量数据,需要存储的数据量很大,要具备水平扩展能力。并且是时序数据(数据随着时间变化,数据很少更改,写入频率高)
  • 大量设备同时上报数据对写入性能要求高
  • 数据存储可靠性要有保证
  • 查询(例如查询某天的设备数据)性能要求高

针对上面的需求,那么我们应该如何选型数据库以及设计存储架构呢???

2. 数据存储对比选型

数据库 适用场景 写入性能 支持水平扩展 查询性能 支持sql语法 存储空间占用 集群
ClickHouse 统计分析查询&&不适合经常修改数据&&不支持事务 性能高,但适用于批量写入 未知 支持 未知
mysql 业务数据增删改查,事务一致性场景 一般 需要分库分表 一般 支持 支持
InfluxDB 数据采集&&大多数是写请求 比TDengine低 支持 未知 未知 未知 集群收费
TDengine 数据采集&&大多数是写请求 性能高,无锁写入 支持 高(比InfluxDB性能高) 支持 集群支持且免费

从上面的适用场景、写入性能、数据存储水平扩展支持、查询性能、集群支持等各个方面比较使用TDengine是最好的选择。

3. 存储架构设计

我们这里的存储架构,主要是使用3台机器部署TDengine集群,保证数据水平扩展、负载均衡以及高可用。

既然数据库选择了TDengine,那么现在开始搭建TDengine服务。肯定不能用单机的吧,毫无疑问,我们使用3个节点部署TDengine集群。每台机器上安装TDengine3.0版本,3.0版本功能更强大,性能更高,详细信息见官方说明。

  1. 添加3个节点的主机名

在每个节点上都加上主机名映射配置

java 复制代码
vi /etc/hosts
192.168.56.200 xg-200
192.168.56.202 xg-202
192.168.56.203 xg-203
  1. 下载TDengine解压安装

在3个节点都安装TDengine

java 复制代码
tar -xzf TDengine-server-3.1.1.0-Linux-x64.tar.gz
./install.sh

安装过程中,出现下面提示,如果是第一个节点,则直接回车创建集群

如果是其他节点,则输入第一节点的hostname和端口(xg-200:6030),以此来加入集群。

  1. 配置节点

修改每个节点的配置文件/etc/taos/taos.cfg

java 复制代码
# firstEp 是每个数据节点首次启动后连接的第一个数据节点
firstEp               xg-200:6030

# 必须配置为本数据节点的 FQDN,如果本机只有一个 hostname,可注释掉本项
fqdn                  xg-200

# 配置本数据节点的端口号,缺省是 6030
serverPort            6030
  1. 启动节点服务
java 复制代码
# 启动服务
systemctl start taosd
# 查看服务状态
systemctl status taosd
# 查看节点及其状态
[root@xg-200 ~]# taos

taos> show dnodes;
     id      |            endpoint            | vnodes | support_vnodes |    sta                                                                             tus    |       create_time       |       reboot_time       |              note                                                                                           |
================================================================================                                                                             ================================================================================                                                                             =============
           1 | xg-200:6030                    |      0 |              4 | ready                                                                                     | 2024-04-04 09:48:58.016 | 2024-04-04 19:20:40.287 |                                                                                                             |
Query OK, 1 row(s) in set (0.016372s)

我们看到第一节点xg-200,并且状态为ready。

然后依次启动其他的数据节点。

java 复制代码
[root@xg-202 TDengine-server-3.1.1.0]# systemctl start taosd
[root@xg-202 TDengine-server-3.1.1.0]# systemctl status taosd
● taosd.service - server service
   Loaded: loaded (/etc/systemd/system/taosd.service; enabled; vendor preset: disabled)
   Active: active (running) since 四 2024-04-04 19:32:00 CST; 20s ago
  Process: 2882 ExecStartPre=/usr/local/taos/bin/startPre.sh (code=exited, status=0/SUCCESS)
 Main PID: 2887 (taosd)
    Tasks: 26
   Memory: 47.3M
   CGroup: /system.slice/taosd.service
           ├─2887 /usr/bin/taosd
           └─2900 /usr/bin/udfd -c /etc/taos/

4月 04 19:32:00 xg-202 systemd[1]: Starting server service...
4月 04 19:32:00 xg-202 systemd[1]: Started server service.
  1. 数据节点加入集群

在第一节点机器上,把其他数据节点添加到集群

java 复制代码
[root@xg-200 ~]# taos
Welcome to the TDengine Command Line Interface, Client Version:3.1.1.0
Copyright (c) 2022 by TDengine, all rights reserved.

...

taos> CREATE DNODE "xg-202:6030";
Create OK, 0 row(s) affected (0.029473s)

taos> SHOW DNODES;
     id      |            endpoint            | vnodes | support_vnodes |    status    |       create_time       |       reboot_time       |              note              |
=============================================================================================================================================================================
           1 | xg-200:6030                    |      0 |              4 | ready        | 2024-04-04 09:48:58.016 | 2024-04-04 19:20:40.287 |                                |
           2 | xg-202:6030                    |      0 |              0 | offline      | 2024-04-04 19:37:04.717 | 1970-01-01 08:00:00.000 | status not received            |
Query OK, 2 row(s) in set (0.004235s)

然后我们看到xg-202节点,状态是offline,表明是离线状态。我看在202上面查看日志

java 复制代码
04/04 19:51:06.545935 00002897 RPC ERROR DND-C msg status failed to send, conn 0x7f53b6cdc480 failed to connect to xg-200:6030, reason: host is unreachable, gtid:0x0:0x73d72a8f2a100062
04/04 19:51:07.551386 00002897 RPC ERROR DND-C msg status failed to send, conn 0x7f53b6cdc480 failed to connect to xg-200:6030, reason: host is unreachable, gtid:0x0:0x73d72a8f2a100062
04/04 19:51:08.558628 00002897 RPC ERROR DND-C msg status failed to send, conn 0x7f53b6cdc480 failed to connect to xg-200:6030, reason: host is unreachable, gtid:0x0:0x73d72a8f2a100062

日志是说,202节点连接到第一节点6030端口失败。可能是200节点上面的防火墙没有对端口放开,我们查看下200上面的防火墙状态,看到防火墙状态时running。

java 复制代码
[root@xg-200 ~]# systemctl status firewalld
● firewalld.service - firewalld - dynamic firewall daemon
   Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled)
   Active: active (running) since 四 2024-04-04 10:08:25 CST; 9h ago
     Docs: man:firewalld(1)
 Main PID: 678 (firewalld)
    Tasks: 2
   Memory: 33.9M
   CGroup: /system.slice/firewalld.service
           └─678 /usr/bin/python2 -Es /usr/sbin/firewalld --nofork --nopid

所以,我们先简单粗暴把防火墙关闭。

java 复制代码
# 关闭防火墙
systemctl stop firewalld.service
# 关闭防火墙自动启动
systemctl disable firewalld.service
# 查看防火墙服务状态
systemctl status firewalld.service

同理,我们在其他机器上也关闭防火墙。

我们再次查看202节点状态,看到状态是ready了,正常了。

java 复制代码
taos> SHOW DNODES;
     id      |            endpoint            | vnodes | support_vnodes |    status    |       create_time       |       reboot_time       |              note              |
=============================================================================================================================================================================
           1 | xg-200:6030                    |      0 |              4 | ready        | 2024-04-04 09:48:58.016 | 2024-04-04 19:20:40.287 |                                |
           2 | xg-202:6030                    |      0 |              4 | ready        | 2024-04-04 19:37:04.717 | 2024-04-04 19:32:01.057 |                                |
Query OK, 2 row(s) in set (0.003572s)

同理,我们添加另外的一个数据节点203到集群

java 复制代码
taos> CREATE DNODE "xg-203:6030";
Create OK, 0 row(s) affected (0.029398s)

taos> SHOW DNODES;
     id      |            endpoint            | vnodes | support_vnodes |    status    |       create_time       |       reboot_time       |              note              |
=============================================================================================================================================================================
           1 | xg-200:6030                    |      0 |              4 | ready        | 2024-04-04 09:48:58.016 | 2024-04-04 19:20:40.287 |                                |
           2 | xg-202:6030                    |      0 |              4 | ready        | 2024-04-04 19:37:04.717 | 2024-04-04 19:32:01.057 |                                |
           3 | xg-203:6030                    |      0 |              4 | ready        | 2024-04-04 20:09:57.535 | 2024-04-04 19:32:06.034 |                                |
Query OK, 3 row(s) in set (0.003004s)

然后,我们查看节点是否正常加入了集群,看到上面203节点已经成功加入了集群,并且状态为ready,加入集群成功。

至此,TDengine集群我们部署成功。如果后期发现数据量继续增大,可以对集群进行水平扩容,扩容的方式就是添加新的节点到集群(上面的步骤已经有说明),之后数据会自动切分到不同的节点。

4. 验证数据存储

我们主要验证:

  • 数据存储的水平扩展
  • 数据存储可靠性

存储集群我们部署完成了,我们需要验证数据水平扩展和可靠性。我们的项目中,设备会上报数据,后面需要从这些设备数据里面查询某天的设备数据,便于汇总生成每天的报告。所以我们肯定需要存储这些设备上报的数据。这些上报的数据随着后期设备的数量增多,数据量会很大,那么数据的水平扩展性和数据存储的高可靠性我们是需要有保证的。

我们先要把TDengine与SpringBoot项目集成

  1. 添加taos-jdbc驱动

我们要存储数据到DB,肯定要先连接DB,taos-jdbc驱动的目的就是连接TDengine数据库,跟我们以往的mysql-jdbc驱动是一个道理。 注意:驱动版本要跟TDengine服务端版本兼容。

taos-jdbcdriver版本 TDengine版本
3.2.7 3.2.0.0 及更高版本
3.2.5 3.1.0.3 及更高版本
3.2.4 -
3.2.3 -
3.2.2 3.0.5.0 及更高版本

由于我们安装的TDengine服务端版本是3.1.1.0,所以我们选择taos-jdbc驱动为3.2.5版本

xml 复制代码
<dependency>
  <groupId>com.taosdata.jdbc</groupId>
  <artifactId>taos-jdbcdriver</artifactId>
  <version>3.2.5</version>
</dependency>
  1. 配置双数据源

我们的项目中除了需要连接TDengine数据库来保存查询设备数据这种时序数据之外,还有其他的一些业务数据需要存储查询,所以还需要连接到mysql数据源。因此就需要有动态的可切换的多个数据源

首先我们引入依赖。特别需要注意:动态数据源版本、Durid连接池版本,如果版本不对,多个数据源会没法动态切换。

yml 复制代码
<!--引入druid数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.9</version>
</dependency>

<!--引入动态数据源依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

配置文件中配置动态的多个数据源mysql数据源和TDengine数据源,mysql是默认数据源。

yml 复制代码
spring:
    datasource:
      type: com.alibaba.druid.pool.DruidDataSource
      dynamic:
        primary: master # 默认数据源
        datasource:
          master:
            url: jdbc:mysql://192.168.56.200:3306/learn?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
            username: root
            password: password
            driver-class-name: com.mysql.jdbc.Driver
          # TDengine数据源
          taosd:
                driver-class-name: com.taosdata.jdbc.TSDBDriver
                # 连接第一数据节点
                url: jdbc:TAOS://192.168.56.200:6030/test_db?timezone=Asia/Beijing&charset=UTF-8
                username: root
                password: taosdata
      druid:
          initialSize: 5
          minIdle: 5
          maxActive: 200
          maxWait: 60000
          timeBetweenEvictionRunsMillis: 60000
          minEvictableIdleTimeMillis: 300000
          validationQuery: SELECT 1 FROM DUAL
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          poolPreparedStatements: false
          filters: stat,wall,log4j2
          # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. 编写Mapper层以及Xml 我们编写Mapper,使用TDengine数据源。
java 复制代码
@Mapper
@DS("taosd")
public interface DeviceDataMapper extends BaseMapper<DeviceDataPO> {

    /**
     * 设备数据批量插入
     * @param list
     */
    void batchInsert(@Param("list") List<DeviceDataDTO> list);
}
java 复制代码
<insert id="batchInsert" parameterType="java.util.List">
    insert into
    <foreach collection="list" item="item" separator=" " close=";">
        tb_device_${item.macAdd} using st_device tags(#{item.macAdd})
        values (#{item.ts}, #{item.userId}, #{item.leftAngle}, #{item.rightAngle})
    </foreach>
</insert>
  1. 创建库表

我们需要先创建数据库test_db,指定vdode副本数(建议至少为3个副本),以此来保证数据的高可用。然后创建超级表。

sql 复制代码
# 创建数据库test_db,数据保留30天,副本数为3,其他参数默认。
taos> CREATE DATABASE test_db KEEP 30 DURATION 10 BUFFER 16 WAL_LEVEL 1 replica 3;
Create OK, 0 row(s) affected (0.529028s)
# 选择数据库
taos> use test_db;
Database changed.
# 接下来还需要创建超级表(记录时间、用户、设备上传的步行的左右倾斜角,设备mac地址)
taos> create stable if not exists st_device (ts TIMESTAMP,
    >   user_id int,
    >   left_angle int,
    >   right_angle int
    > ) TAGS (mac_add BINARY(50));
Create OK, 0 row(s) affected (0.038011s)
  1. 测试写入数据
java 复制代码
// 模拟设备每天的数据
List<DeviceDataDTO> deviceDatalist = new ArrayList<>();
for(int i = 1; i <= 6; i++) {
    for(int j = 10; j <= 16; j++) {
        DeviceDataDTO deviceDataDTO = new DeviceDataDTO();
        Timestamp timestamp = Timestamp.valueOf(String.format("2024-04-0%s %s:05:03", i, j));
        deviceDataDTO.setTs(timestamp);
        deviceDataDTO.setUserId(1);
        deviceDataDTO.setMacAdd("aaa");
        // 步行时身体的左倾角度
        deviceDataDTO.setLeftAngle(RandomUtil.randomInt(1, 100));
        // 步行时身体的右倾角度
        deviceDataDTO.setRightAngle(RandomUtil.randomInt(1, 100));
        deviceDatalist.add(deviceDataDTO);
    }
}
log.info("deviceDatalist: {}", JSON.toJSONString(deviceDatalist));
deviceDataService.batchInsert(deviceDatalist);

我们看到设备数据写入TDengine成功了。

sql 复制代码
taos> select * from st_device;
           ts            |   user_id   | left_angle  | right_angle |            mac_add             |
=====================================================================================================
 2024-04-01 10:05:03.000 |           1 |          50 |          11 | aaa                            |
 2024-04-01 11:05:03.000 |           1 |          59 |          73 | aaa                            |
 2024-04-01 12:05:03.000 |           1 |          21 |          43 | aaa                            |
 2024-04-01 13:05:03.000 |           1 |          92 |          47 | aaa                            |
 2024-04-01 14:05:03.000 |           1 |           5 |          22 | aaa                            |
 2024-04-01 15:05:03.000 |           1 |          34 |          30 | aaa                            |
 2024-04-01 16:05:03.000 |           1 |          85 |          39 | aaa                            |
 2024-04-02 10:05:03.000 |           1 |          26 |          46 | aaa                            |
 2024-04-02 11:05:03.000 |           1 |          53 |          87 | aaa                            |
 2024-04-02 12:05:03.000 |           1 |          97 |          98 | aaa                            |
 2024-04-02 13:05:03.000 |           1 |          23 |          76 | aaa                            |
 2024-04-02 14:05:03.000 |           1 |          45 |          77 | aaa                            |
 2024-04-02 15:05:03.000 |           1 |          89 |          93 | aaa                            |
 2024-04-02 16:05:03.000 |           1 |          81 |          14 | aaa                            |
 2024-04-03 10:05:03.000 |           1 |          18 |           7 | aaa                            |
 2024-04-03 11:05:03.000 |           1 |          83 |          17 | aaa                            |
 2024-04-03 12:05:03.000 |           1 |          39 |          19 | aaa                            |
 2024-04-03 13:05:03.000 |           1 |           5 |          89 | aaa                            |
 2024-04-03 14:05:03.000 |           1 |          36 |          47 | aaa                            |
 2024-04-03 15:05:03.000 |           1 |          48 |          12 | aaa                            |
 2024-04-03 16:05:03.000 |           1 |          68 |          73 | aaa                            |

我们看到数据被切分到了2个vnode虚拟节点,同时这2个vnode节点分布到不同的dnode物理节点。从而实现了水平扩展负载均衡

java 复制代码
# 200节点
[root@xg-200 vnode]# ll
总用量 4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode5
-rwxrwxrwx. 1 root root 169 4月   9 21:45 vnodes.json

# 202节点
[root@xg-202 vnode]# ll
总用量 4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode5
-rwxrwxrwx. 1 root root 169 4月   9 21:45 vnodes.json

# 203节点
[root@xg-203 vnode]# ll
总用量 4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode4
drwxr-xr-x. 7 root root  81 4月   9 21:45 vnode5
-rwxrwxrwx. 1 root root 169 4月   9 21:45 vnodes.json

taos> show dnodes;
     id      |            endpoint            | vnodes | support_vnodes |    status    |       create_time       |       reboot_time       |              note              |
=============================================================================================================================================================================
           1 | xg-200:6030                    |      2 |              4 | ready        | 2024-04-04 09:48:58.016 | 2024-04-09 21:25:53.668 |                                |
           2 | xg-202:6030                    |      2 |              4 | ready        | 2024-04-04 19:37:04.717 | 2024-04-09 21:25:50.637 |                                |
           3 | xg-203:6030                    |      2 |              4 | ready        | 2024-04-04 20:09:57.535 | 2024-04-09 21:25:55.682 |                                |
Query OK, 3 row(s) in set (0.011171s)

同时我们从上面的信息看到,每个vnode虚拟节点都有3个副本,分布在不同的dnode物理节点上,保证了数据的高可用

5. 验证查询数据

我们的项目中经常有这样的需求:根据每天的设备数据来生成每天的用户报告,所以需要查询某天的设备数据。

查询某天的设备数据的mapper和xml

java 复制代码
/**
 * 查询某天的设备数据
 * @param macAdd
 * @param startTime
 * @param endTime
 * @return 
 */
List<DeviceDataPO> selectByOneDay(@Param("macAdd") String macAdd,
                                  @Param("startTime") Timestamp startTime,
                                  @Param("endTime") Timestamp endTime);
xml 复制代码
<!--查询设备某天的数据-->
<select id="selectByOneDay" resultMap="BaseResultMap">
    select * from st_device
    where mac_add = #{macAdd}
    and ts &gt;= #{startTime} and ts &lt;= #{endTime}
</select>

我们启动应用,发现报错:

java 复制代码
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class

发现是创建dataSource数据源异常,需要在启动类排除掉DruidDataSourceAutoConfigure依赖

java 复制代码
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

我们查看到TDengine的官方文档,发现运行程序连接TDengine,需要在运行程序的机器上安装TDengine客户端驱动注意:这里的驱动跟前面步骤中的taos-jdbc驱动不是一个东西,不要搞混淆了。我们本机是window系统,则安装TDengine-client-3.1.1.0-Windows-x64.exe,特别注意:这里的客户端版本需要跟服务端版本一致。如果不一致会报如下错:版本不兼容。

java 复制代码
 JNI ERROR (0x2354): Version not compatible

最后还有一个地方我们需要注意:应用程序所在机器需要配置host主机映射。不然会连接异常。

java 复制代码
# TDengine
192.168.56.200  xg-200
192.168.56.202  xg-202
192.168.56.203  xg-203

最后验证查询某天的设备数据。

java 复制代码
String macAdd = "aaa";
Timestamp startTime = Timestamp.valueOf("2024-04-06 10:05:03.000");
Timestamp endTime = Timestamp.valueOf("2024-04-06 16:05:03.000");
List<DeviceDataPO> deviceDataPOList = deviceDataService.selectByOneDay(macAdd, startTime, endTime);
log.info("deviceDataPOList: {}", JSON.toJSONString(deviceDataPOList));

返回7条数据

java 复制代码
deviceDataPOList: [{"leftAngle":85,"rightAngle":29,"ts":1712369103000,"userId":1},{"leftAngle":84,"rightAngle":63,"ts":1712372703000,"userId":1},{"leftAngle":28,"rightAngle":51,"ts":1712376303000,"userId":1},{"leftAngle":71,"rightAngle":26,"ts":1712379903000,"userId":1},{"leftAngle":60,"rightAngle":1,"ts":1712383503000,"userId":1},{"leftAngle":74,"rightAngle":13,"ts":1712387103000,"userId":1},{"leftAngle":84,"rightAngle":36,"ts":1712390703000,"userId":1}]

6. 总结

  1. 我们主要介绍了在物联网场景中(当然不局限物联网场景),设备上报数据时面临如下情况:
  • 大量设备数据高频写入,并且是时序数据(数据随着时间变化,数据很少更改,写入频率高)
  • 海量数据存储
  • 查询效率高(毫秒级别)
  • 高可用,横向扩展
  • 节省建设成本(CPU、内存、磁盘占用低)
  • 数据保留存储
  • 兼容sql语法,学习成本低
  • 集群支持
  • 数据采集监控统计

在面临上面这些需求等场景下的数据库选型、存储架构的简单部署、以及测试验证。我们发现此种场景使用TDengine时序数据库更适合。如果大家有类似的场景,也可以使用TDengine。

  1. 然后我们是部署了3个节点的TDengine集群,保证数据水平扩展负载均衡以及高可用性。当然如果数据继续增长,我们还可以继续添加新的节点来水平扩展。

  2. 最后,我们是验证了数据存储的水平扩展、数据存储可靠性。还有数据的查询(以查询某天的设备数据为例子)。

相关推荐
学地理的小胖砸42 分钟前
【Python 操作 MySQL 数据库】
数据库·python·mysql
dddaidai1231 小时前
Redis解析
数据库·redis·缓存
数据库幼崽1 小时前
MySQL 8.0 OCP 1Z0-908 121-130题
数据库·mysql·ocp
Amctwd2 小时前
【SQL】如何在 SQL 中统计结构化字符串的特征频率
数据库·sql
betazhou2 小时前
基于Linux环境实现Oracle goldengate远程抽取MySQL同步数据到MySQL
linux·数据库·mysql·oracle·ogg
lyrhhhhhhhh3 小时前
Spring 框架 JDBC 模板技术详解
java·数据库·spring
喝醉的小喵4 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
付出不多4 小时前
Linux——mysql主从复制与读写分离
数据库·mysql
初次见面我叫泰隆4 小时前
MySQL——1、数据库基础
数据库·adb
Chasing__Dreams4 小时前
Redis--基础知识点--26--过期删除策略 与 淘汰策略
数据库·redis·缓存