作者|马龙,融合通信高级工程师
小 T 导读:融合通信技术(天津)有限公司是具备国内一流水准的企业移动新通信商务平台技术和应用方案提供商。融合短信、彩信、语音、流量及虚拟座席的云通信服务平台, 融合三大运营商和互联网IP网络,致力于为国内外企业提供一流水准的移动新通信商务平台及运营服务。
公司的主营业务是给企业客户提供短信服务,内部有一个强大的运营监控平台。每日短信业务量巨大,监控平台需要使用时序数据库(Time-Series Database)来对发送的海量短信分地理区域、运营商、接收状态等类别进行统计和监控。以前一直在用InfluxDB,对于时间跨度稍大些的查询(比如一个月的数据)就显得非常慢了。从TDengine Database开源后,便知道了这个很牛的家伙,于是尝试着使用TDengine。
在搞懂了基本的功能后,便上线了TDengine版的监控系统,但是发现,在grafana中居然不能“group by”,只能通过写where多条语句将多个短信状态数据放到一个仪表盘里,如图:
如果where条件有更多,这样的方法就太笨,并且灵活性太差。
于是,开始仔细研究官方文档,搞懂了“超级表”、“连续查询”等,在这个过程中遇到过不少问题,在这里做一下记录(测试环境,数据是模拟产生的)。
一、安装和运行
测试环境:
cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)
安装很简单:
rpm -ivh tdengine-1.6.3.1-3.x86_64.rpm
配置都用默认的(未改配置文件)
启动TDengine:
systemctl start taosd
二、建库、建表
在命令行输入“taos”
taos>
以下建库、建表操作都在此提示符下进行
1. 创建数据库
create database jk keep 365 precision 'us';
说明:
- keep 365表示该库保存365天内的数据,365天之前的数据会被自动清除(因为是时序数据库,所以不能对库中的表进行delete操作);
- precision ‘us’表示数据库中的时间戳精度为“微秒”,(默认是毫秒,也可以显示写为precision ‘ms’), 经测试,此处用单引号和双引号都是可以的,但是不能没有引号。
- 配置文件中已经不支持时间精度的配置,必须在建库的时候指定(测试了很多配置都不生效,Issues后得到的官方回复)
- TDengine的设计初衷是用于物联网,设备的信息采集精度到“毫秒”已经足够用,但我司的短信平台会有突发大量数据产生,为了避免可能会导致的数据丢失,将精度设置为“微秒”,经测试,效果很好,测试的模拟数据插入时间戳都用“now”获取,下图左侧为“毫秒”精度,能看到有“0 row(s)”的情况出现,表示有数据未插入, 右侧为“微秒”精度,未见未插入数据。
2. 创建超级表
进入数据库“jk”
taos> use jk;
Database changed.
create table jiankong (ts timestamp, gatewayid binary(6), companyid binary(20), provinceid binary(10), cityid binary(10), value int, timestr binary(30))
tags(type binary(10), subtype binary(10));
短信系统中有3种type,几百种subtype,所以将这些静态信息(或者简单地理解为需要将进行group by的字段设置为tag)
解释一下超级表:
STable是同一类型数据采集点的抽象,是同类型采集实例的集合,包含多张数据结构一样的子表。
每个STable为其子表定义了表结构和一组标签:表结构即表中记录的数据列及其数据类型;标签名和数据类型由STable定义,标签值记录着每个子表的静态信息,用以对子表进行分组过滤。
子表本质上就是普通的表,由一个时间戳主键和若干个数据列组成,每行记录着具体的数据,数据查询操作与普通表完全相同;但子表与普通表的区别在于每个子表从属于一张超级表,并带有一组由STable定义的标签值。
针对所有的通过STable创建的子表进行多表聚合查询,支持按照全部的TAG值进行条件过滤(where),并可将结果按照TAGS中的值进行聚合(group by),暂不支持针对binary类型的模糊匹配过滤。
标签数据(或者叫标签值)直接关联到每个子表,相同的标签值(一个或者多个,最多6个)定位到一个子表(用“写数据时自动建表”的方式,可以将“相同的标签值”对应到多个子表)。
tag值支持中文,需要将tag类型设置为NCHAR(在其他测试中已经验证过)。
3. 创建子表(子表就是普通表,表结构完全由超级表定义):
create table jiankong_sub_send using jiankong tags ('send', 'send');
create table jiankong_sub_delivrd using jiankong tags ('delivrd', 'delivrd');
create table jiankong_sub_undeliv_db_0108 using jiankong tags ('undeliv', 'DB:0108');
create table jiankong_sub_undeliv_db_0107 using jiankong tags ('undeliv', 'DB:0107');
create table jiankong_sub_undeliv_balance using jiankong tags ('undeliv', 'BALANCE');
create table jiankong_sub_undeliv_id_0076 using jiankong tags ('undeliv', 'ID:0076');
create table jiankong_sub_undeliv_ib_0008 using jiankong tags ('undeliv', 'IB:0008');
相同类型的type和subtype的组合创建为一个子表(只是进行测试,所以并没有将所有的几百个subtype都进行建表)
4. 插入数据
INSERT INTO jiankong_sub_send VALUES (now, 3034, '1564', '109', '1272', '2', '201909231530')
INSERT INTO jiankong_sub_delivrd VALUES (now, 3034, '1564', '109', '1272', '2', '201909231530')
INSERT INTO jiankong_sub_undeliv_balance VALUES (now, 1179, '152', '106', '1000', '1', '201910071113')
INSERT INTO jiankong_sub_undeliv_id_0076 VALUES (now, 1165, '1785', '111', '1226', '1', '201910071415')
INSERT INTO jiankong_sub_undeliv_ib_0008 VALUES (now, 1165, '1785', '127', '1000', '2', '201910061727')
INSERT INTO jiankong_sub_undeliv_db_0108 VALUES (now, 90, '548', '123', '1237', '1', '201910061127')
INSERT INTO jiankong_sub_undeliv_db_0107 VALUES (now, 2261, '808', '116', '1314', '2', '201910032106')
以上是对上述创建的7个子表分别插入模拟数据,由于模拟大量数据,所以需要写shell脚本(也可以用其他方式)进行数据灌入。
写入数据时不能直接对STable操作,而是要对每张子表进行操作。
5. 数据库、表结构等查询
查询数据库信息:
taos> show databases;
name | created time | ntables | vgroups |replica| days | keep1,keep2,keep(D) | tables | rows | cache(b) | ablocks |tblocks| ctime(s) | clog | comp |time precision| status |
==============================================================================================================================================================================================================================================
log | 19-11-18 16:37:14.025| 4| 1| 1| 10|30,30,30 | 32| 1024| 2048| 2.00000| 32| 3600| 1| 2|us |ready |
jk | 19-11-18 16:48:19.867| 10| 1| 1| 10|365,365,365 | 1024| 4096| 16384| 4.00000| 100| 3600| 1| 2|us |ready |
Query OK, 1 row(s) in set (0.002487s)
查询超级表:
taos> show stables;
name | created_time |columns| tags | tables |
====================================================================================================================
jiankong | 19-11-18 16:48:41.540| 7| 2| 7| Query OK, 1 row(s) in set (0.002140s)
查询超级表的表结构:
taos> describe jiankong;
Field | Type | Length | Note |
=======================================================================================================
ts |TIMESTAMP | 8| |
gatewayid |BINARY | 6| |
companyid |BINARY | 20| |
provinceid |BINARY | 10| |
cityid |BINARY | 10| | value |INT | 4| |
timestr |BINARY | 30| |
type |BINARY | 10|tag |
subtype |BINARY | 10|tag |
Query OK, 9 row(s) in set (0.001301s)
可以在Note列看到“tag”,表示是此列是标签
查询子表:
taos> show tables;
table_name | created_time |columns| stable |
=================================================================================================================================================================
jiankong_sub_delivrd | 19-11-18 16:49:17.009| 7|jiankong |
jiankong_sub_undeliv_ib_0008 | 19-11-18 16:49:17.025| 7|jiankong |
jiankong_sub_undeliv_db_0108 | 19-11-18 16:49:17.016| 7|jiankong |
jiankong_sub_undeliv_db_0107 | 19-11-18 16:49:17.018| 7|jiankong |
jiankong_sub_undeliv_id_0076 | 19-11-18 16:49:17.023| 7|jiankong |
jiankong_sub_send | 19-11-18 16:49:17.003| 7|jiankong |
jiankong_sub_undeliv_balance | 19-11-18 16:49:17.021| 7|jiankong |
查询具体的子表的表结构:
taos> describe jiankong_sub_undeliv_db_0108;
Field | Type | Length | Note |
=========================================================================================================
ts |TIMESTAMP | 8| |
gatewayid |BINARY | 6| |
companyid |BINARY | 20| |
provinceid |BINARY | 10| |
cityid |BINARY | 10| |
value |INT | 4| |
timestr |BINARY | 30| |
type |BINARY | 10|undeliv |
subtype |BINARY | 10|DB:0108 |
可以在Note列看到“undeliv”(超级表中的type字段)和“DB:0108″(超级表中的subtype字段),这2个静态标签值确定了这个子表
6. 数据查询
对type进行分组聚合查询:
taos> select sum(value) from jk.jiankong group by type;
sum(value) | type |
=================================
11827688|delivrd |
55566578|send |
46687487|undeliv |
Query OK, 3 row(s) in set (0.018251s)
对subtype进行分组聚合查询:
taos>
taos> select sum(value) from jk.jiankong group by subtype;
sum(value) | subtype |
=================================
9317|BALANCE |
65219|DB:0107 |
2077691|DB:0108 |
2804417|IB:0008 |
41730843|ID:0076 |
11827688|delivrd |
55566578|send |
Query OK, 7 row(s) in set (0.013978s)
对type和subtype进行分组聚合查询:
taos> select sum(value) from jk.jiankong group by type, subtype;
sum(value) | type | subtype |
============================================
11827688|delivrd |delivrd |
55566578|send |send |
9317|undeliv |BALANCE |
65219|undeliv |DB:0107 |
2077691|undeliv |DB:0108 |
2804417|undeliv |IB:0008 |
41730843|undeliv |ID:0076 |
Query OK, 7 row(s) in set (0.732830s)
按天对type和subtype进行分组聚合查询:
taos> select sum(value) from jk.jiankong interval(1d) group by type, subtype;
ts | sum(value) | type | subtype |
======================================================================
19-11-18 00:00:00.000000| 1760800|delivrd |delivrd |
19-11-19 00:00:00.000000| 14768|delivrd |delivrd |
19-11-20 00:00:00.000000| 3290720|delivrd |delivrd |
19-11-21 00:00:00.000000| 4973640|delivrd |delivrd |
19-11-22 00:00:00.000000| 1787760|delivrd |delivrd |
19-11-18 00:00:00.000000| 36976790|send |send |
19-11-19 00:00:00.000000| 310128|send |send |
19-11-20 00:00:00.000000| 9482760|send |send |
19-11-21 00:00:00.000000| 6470940|send |send |
19-11-22 00:00:00.000000| 2325960|send |send |
19-11-18 00:00:00.000000| 6200|undeliv |BALANCE |
19-11-19 00:00:00.000000| 52|undeliv |BALANCE |
19-11-20 00:00:00.000000| 1590|undeliv |BALANCE |
19-11-21 00:00:00.000000| 1085|undeliv |BALANCE |
19-11-22 00:00:00.000000| 390|undeliv |BALANCE |
19-11-18 00:00:00.000000| 43400|undeliv |DB:0107 |
19-11-19 00:00:00.000000| 364|undeliv |DB:0107 |
19-11-20 00:00:00.000000| 11130|undeliv |DB:0107 |
19-11-21 00:00:00.000000| 7595|undeliv |DB:0107 |
19-11-22 00:00:00.000000| 2730|undeliv |DB:0107 |
19-11-18 00:00:00.000000| 1382600|undeliv |DB:0108 |
19-11-19 00:00:00.000000| 11596|undeliv |DB:0108 |
19-11-20 00:00:00.000000| 354570|undeliv |DB:0108 |
19-11-21 00:00:00.000000| 241955|undeliv |DB:0108 |
19-11-22 00:00:00.000000| 86970|undeliv |DB:0108 |
19-11-18 00:00:00.000000| 1866200|undeliv |IB:0008 |
19-11-19 00:00:00.000000| 15652|undeliv |IB:0008 |
19-11-20 00:00:00.000000| 478590|undeliv |IB:0008 |
19-11-21 00:00:00.000000| 326585|undeliv |IB:0008 |
19-11-22 00:00:00.000000| 117390|undeliv |IB:0008 |
19-11-18 00:00:00.000000| 27769800|undeliv |ID:0076 |
19-11-19 00:00:00.000000| 232908|undeliv |ID:0076 |
19-11-20 00:00:00.000000| 7121610|undeliv |ID:0076 |
19-11-21 00:00:00.000000| 4859715|undeliv |ID:0076 |
19-11-22 00:00:00.000000| 1746810|undeliv |ID:0076 |
Query OK, 35 row(s) in set (0.023865s)
此处interval是聚合时间段的长度, 最短时间间隔10毫秒(10a)
未建超级表时,对普通表进行分组聚合查询,会报错:
taos> select sum(value) from jk.jiankong group by type;
TSDB error: invalid SQL: group by only available for STable query
7. 写数据时自动建子表
我们有另外一个需求,由于要监控的静态数据多达几百个,而且具有不确定性,所以无法全部在建库、建表的时候创建所有子表,这个功能完全解决了我们的这个问题。
以下是官网的文档摘录:
在某些特殊场景中,用户在写数据时并不确定某个设备的表是否存在,此时可使用自动建表语法来实现写入数据时用超级表定义的表结构自动创建不存在的子表,若该表已存在则不会建立新表。
INSERT INTO <tb_name> USING <stb_name> TAGS (<tag1_value>, …) VALUES (field_value, …) (field_value, …) …;
注意:自动建表语句只能自动建立子表而不能建立超级表,这就要求超级表已经被事先定义好。自动建表语法跟insert/import语法非常相似,唯一区别是语句中增加了超级表和标签信息。具体语法如下:
对比,用create创建子表:
create table jiankong_sub_send using jiankong tags ('send', 'send');
三、安装和配置garafana
1. 安装
在官网https://grafana.com/grafana/download下载grafana的rpm安装包后,进行安装:
rpm -ivh grafana-6.4.4-1.x86_64.rpm
2. copy TDengine的Grafana插件到Grafana的插件目录
TDengine的Grafana插件在安装包的/usr/local/taos/connector/grafana目录下
cp -r /usr/local/taos/connector/grafana/tdengine/ /var/lib/grafana/plugins
3. 启动Grafana
systemctl start grafana-server
4. 在浏览器中通过host:3000登录Grafana服务器
默认用户名和密码都是admin
5. 添加TDengine数据源
在最下方找到“TDengine”
Name:“TDengine”(可以是其他名字)
Host:测试服务器地址“http://192.168.8.66:6020”
User: 默认为“root”
Password:默认为“taosdata”
测试一下:
6. 添加Folder
将相同类型需要监控的仪表盘(dashboard)放到一个Folder中
7. 添加Dashboard
进入刚才创建的Folder后,创建Dashboard
INPUT处的sql语句,要注意fill的位置,需要在group by前面,否则报错
配置图形显示
可以根据需求进行曲线图,表格,仪表盘等的选择
在这里配置“曲线图”,图示下方是具体的图形显示细则,如:标线、是否填充,对显示的字段进行曲线颜色的自定义等
给此仪表盘起个一看就懂的名字:
8. 配置了6个仪表盘
9. 无group by和有group by的实例
无group by,需要写多条sql,通过where条件去区分,如果分类多就很麻烦且不灵活
有group by,一条sql就解决问题了:
下图是有无group by的曲线趋势对比,可以看到是一模一样的
四、高级功能初探和一些联想
1. 连续查询(Continuous Query)—-用户级别的预计算
TDengine的“预计算”的概念我觉得非常的棒,官网文档摘录如下:
为有效地提升查询处理的性能,针对物联网数据的不可更改的特点,TDengine采用在每个保存的数据块上,都记录下该数据块中数据的最大值、最小值、和等统计数据。如果查询处理涉及整个数据块的全部数据,则直接使用预计算结果,不再读取数据块的内容。由于预计算模块的大小远小于磁盘上存储的具体数据的大小,对于磁盘IO为瓶颈的查询处理,使用预计算结果可以极大地减小读取IO,并加速查询处理的流程。
连续查询的官网文档摘录如下:
基于滑动窗口的流式计算(Stream)
连续查询是TDengine定期自动执行的查询,采用滑动窗口的方式进行计算,是一种简化的时间驱动的流式计算。针对库中的表或超级表,TDengine可提供定期自动执行的连续查询,用户可让TDengine推送查询的结果,也可以将结果再写回到TDengine中。每次执行的查询是一个时间窗口,时间窗口随着时间流动向前滑动。在定义连续查询的时候需要指定时间窗口(time window, 参数interval )大小和每次前向增量时间(forward sliding times, 参数sliding)。
其中,将结果再写回到TDengine中的方式其实就是一种用户级别的预计算,这样由TDengine按照用户定义的时间窗口和时间增量进行后台的计算,在用户查询数据的时候,直接从回写的表中读取数据,速度就会非常快。
创建连续查询:
taos> create table test_stream_sum as select sum(value) from jiankong interval(20s) sliding(10s) group by type, subtype;
Query OK, 1 row(s) affected (0.000983s)
上述连续查询,sql的select部分实际的输出结果为:
taos> select sum(value) from jiankong interval(20s) group by type, subtype;
ts | sum(value) | type | subtype |
======================================================================
19-11-18 16:50:40.000000| 9088|delivrd |delivrd |
19-11-18 16:51:00.000000| 31808|delivrd |delivrd |
19-11-18 16:51:20.000000| 15904|delivrd |delivrd |
19-11-18 16:52:20.000000| 12212|delivrd |delivrd |
19-11-18 16:52:40.000000| 31524|delivrd |delivrd |
19-11-18 16:53:00.000000| 31524|delivrd |delivrd |
19-11-18 16:53:20.000000| 31808|delivrd |delivrd |
19-11-18 16:53:40.000000| 31240|delivrd |delivrd |
19-11-18 16:54:00.000000| 31524|delivrd |delivrd |
19-11-18 16:54:20.000000| 31524|delivrd |delivrd |
19-11-18 16:54:40.000000| 31240|delivrd |delivrd |
19-11-18 16:55:00.000000| 31524|delivrd |delivrd |
19-11-18 16:55:20.000000| 28400|delivrd |delivrd |
19-11-18 16:55:40.000000| 31808|delivrd |delivrd |
19-11-18 16:56:00.000000| 31524|delivrd |delivrd |
19-11-18 16:56:20.000000| 31240|delivrd |delivrd |
19-11-18 16:56:40.000000| 31524|delivrd |delivrd |
19-11-18 16:57:00.000000| 32092|delivrd |delivrd |
19-11-18 16:57:20.000000| 31240|delivrd |delivrd |
19-11-18 16:57:40.000000| 32092|delivrd |delivrd |
19-11-18 16:58:00.000000| 31240|delivrd |delivrd |
19-11-18 16:58:20.000000| 22720|delivrd |delivrd |
19-11-18 16:50:40.000000| 190848|send |send |
自动创建的连续查询的表中实际的数据为:
taos> select * from test_stream_sum;
ts | sum_value_ |
================================================
19-11-18 17:17:30.000000| 2556|
19-11-18 17:17:40.000000| 18460|
19-11-18 17:17:50.000000| 15904|
19-11-18 17:18:00.000000| 15620|
Query OK, 4 row(s) in set (0.000431s)
上述结果并不是期待的结果,没有按照我定义的group by字段进行聚合查询显示。
于是github Issues, taos的攻城狮回复“连续查询目前还不能很好的支持 group by,这个问题已经在我们的计划列表里,后续会完善这方面的功能”,原因是“因为这样回写后,新表的时间戳主键可能出现相同(不同的group by字段会有相同的时间),就会冲突了,所以暂时不支持”。
虽然目前这个功能未达到我的预期,攻城狮的回复还是聊以安慰,期待这个功能的完善。
2. 联想
TDengine的设计初衷是服务于物联网,有结构化和时序的特性,建议是和时序密切的数据可以用,其他的数据不建议使用。
从TDengine的架构设计、存储等等,觉得不局限于百分之百的时序特性,有些明细查询也是可以尝试在TDengine中进行存储的,后续会尝试进行一下测试。