掘金 后端 ( ) • 2024-04-13 11:24

theme: healer-readable highlight: a11y-dark

1. 背景

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

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

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

2. 数据存储对比选型

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

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

3. 存储架构设计

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

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

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

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

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

tar -xzf TDengine-server-3.1.1.0-Linux-x64.tar.gz
./install.sh

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

image.png

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

image.png

  1. 配置节点

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

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

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

# 配置本数据节点的端口号,缺省是 6030
serverPort            6030

  1. 启动节点服务
# 启动服务
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。

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

[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. 数据节点加入集群

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

[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上面查看日志


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。

[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

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

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

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

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

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到集群


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版本

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

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

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

<!--引入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是默认数据源。

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数据源。
@Mapper
@DS("taosd")
public interface DeviceDataMapper extends BaseMapper<DeviceDataPO> {

    /**
     * 设备数据批量插入
     * @param list
     */
    void batchInsert(@Param("list") List<DeviceDataDTO> list);
}
<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个副本),以此来保证数据的高可用。然后创建超级表。

# 创建数据库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. 测试写入数据
// 模拟设备每天的数据
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成功了。

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物理节点。从而实现了水平扩展负载均衡

# 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

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

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

Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class

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

@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,特别注意:这里的客户端版本需要跟服务端版本一致。如果不一致会报如下错:版本不兼容。

 JNI ERROR (0x2354): Version not compatible

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

# TDengine
192.168.56.200  xg-200
192.168.56.202  xg-202
192.168.56.203  xg-203

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

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条数据

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