1. 背景
我们的物联网智能硬件项目,通常会有各种硬件模组上报数据,这些数据需要存储起来便于分析查询。我们一般有如下需求:
- 硬件模组持续不断高频率上报大量数据,需要存储的数据量很大,要具备水平扩展能力。并且是时序数据(数据随着时间变化,数据很少更改,写入频率高)
- 大量设备同时上报数据对写入性能要求高
- 数据存储可靠性要有保证
- 查询(例如查询某天的设备数据)性能要求高
针对上面的需求,那么我们应该如何选型数据库以及设计存储架构呢???
2. 数据存储对比选型
数据库 | 适用场景 | 写入性能 | 支持水平扩展 | 查询性能 | 支持sql语法 | 存储空间占用 | 集群 |
---|---|---|---|---|---|---|---|
ClickHouse | 统计分析查询&&不适合经常修改数据&&不支持事务 | 性能高,但适用于批量写入 | 未知 | 高 | 支持 | 小 | 未知 |
mysql | 业务数据增删改查,事务一致性场景 | 一般 | 需要分库分表 | 一般 | 支持 | 高 | 支持 |
InfluxDB | 数据采集&&大多数是写请求 | 比TDengine低 | 支持 | 未知 | 未知 | 未知 | 集群收费 |
TDengine | 数据采集&&大多数是写请求 | 性能高,无锁写入 | 支持 | 高(比InfluxDB性能高) | 支持 | 小 | 集群支持且免费 |
从上面的适用场景、写入性能、数据存储水平扩展支持、查询性能、集群支持等各个方面比较使用TDengine
是最好的选择。
3. 存储架构设计
我们这里的存储架构,主要是使用3台机器部署TDengine集群,保证数据水平扩展、负载均衡以及高可用。
既然数据库选择了TDengine,那么现在开始搭建TDengine服务。肯定不能用单机的吧,毫无疑问,我们使用3个节点部署TDengine集群。每台机器上安装TDengine3.0版本,3.0版本功能更强大,性能更高,详细信息见官方说明。
- 添加3个节点的主机名
在每个节点上都加上主机名映射配置
java
vi /etc/hosts
192.168.56.200 xg-200
192.168.56.202 xg-202
192.168.56.203 xg-203
- 下载TDengine解压安装
在3个节点都安装TDengine
java
tar -xzf TDengine-server-3.1.1.0-Linux-x64.tar.gz
./install.sh
安装过程中,出现下面提示,如果是第一个节点,则直接回车创建集群
如果是其他节点,则输入第一节点的hostname和端口(xg-200:6030),以此来加入集群。
- 配置节点
修改每个节点的配置文件/etc/taos/taos.cfg
java
# firstEp 是每个数据节点首次启动后连接的第一个数据节点
firstEp xg-200:6030
# 必须配置为本数据节点的 FQDN,如果本机只有一个 hostname,可注释掉本项
fqdn xg-200
# 配置本数据节点的端口号,缺省是 6030
serverPort 6030
- 启动节点服务
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.
- 数据节点加入集群
在第一节点机器上,把其他数据节点添加到集群
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项目集成
- 添加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>
- 配置双数据源
我们的项目中除了需要连接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
- 编写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>
- 创建库表
我们需要先创建数据库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)
- 测试写入数据
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 >= #{startTime} and ts <= #{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. 总结
- 我们主要介绍了在物联网场景中(当然不局限物联网场景),设备上报数据时面临如下情况:
- 大量设备数据高频写入,并且是时序数据(数据随着时间变化,数据很少更改,写入频率高)
- 海量数据存储
- 查询效率高(毫秒级别)
- 高可用,横向扩展
- 节省建设成本(CPU、内存、磁盘占用低)
- 数据保留存储
- 兼容sql语法,学习成本低
- 集群支持
- 数据采集监控统计
在面临上面这些需求等场景下的数据库选型、存储架构的简单部署、以及测试验证。我们发现此种场景使用TDengine时序数据库
更适合。如果大家有类似的场景,也可以使用TDengine。
-
然后我们是部署了3个节点的TDengine集群,保证数据水平扩展负载均衡以及高可用性。当然如果数据继续增长,我们还可以继续添加新的节点来水平扩展。
-
最后,我们是验证了数据存储的水平扩展、数据存储可靠性。还有数据的查询(以查询某天的设备数据为例子)。