松果时序数据库(PinusDB)是一款以简单、易用、高性能为目标的开源时序数据库。本篇比较全面介绍松果时序数据库的测试,为用户测试选型提供参考。
松果时序数据库提供差值压缩,测试数据对压缩性能有着极大的影响,为此我们调研了一些公开的数据集,最后选择timescaledb官网提供的物联网设备的传感器信息来测试,数据集包含3000个设备每个设备1万条数据,总共3000万条数据。
数据集下载地址:https://timescaledata.blob.core.windows.net/datasets/devices_big.tar.gz
一 测试环境
CPU: Intel Core i3-7100 (双核4线程 3.9GHz)
内存: 8GB
硬盘:
C盘:120GB SSD 安装操作系统
D盘:1TB HDD
E盘:120GB SSD
操作系统:Windows Server 2016
二 服务部署
松果时序数据库安装过程请参考官网相应文档,这里仅说明配置文件的修改。
设置缓存大小为4GB,即:cacheSize=4096
数据集时间为2016年,故设置有效插入时间为2000,也就是允许写入距离当前时间2000天内的数据,即:insertValidDay=2000
设置表目录、普通文件目录到E盘,设置压缩数据目录、commit日志目录及系统日志目录到D盘。
完整服务配置如下:
三 数据写入
我们使用TimescaleDB官网提供的devices_readings_big.csv文件内的数据作为测试数据集。这个文件内数据的结构如下所示:
其中,bssid 与 ssid 字段是tag信息(这个请参考一下其他时序数据库的概念),每个设备的所有数据都不会有变化,所以这里测试建表时将这两个字段排除。
device_id是设备标识,取值为demo000000 ~ demo002999,在松果时序数据库中设备标识为大于0的整数,所以我们将device_id的后六位转为数字后加10000,也就是在松果时序数据库中devid的范围为 10000~12999;所有的double类型以real3类型存储,即:保留3个小数位(real系列数据类型能提供更高的压缩性能)。
最终创建表的语句如下:
create table readings
(
devid bigint,
tstamp datetime,
battery_level bigint,
battery_status string,
battery_temperature real3,
cpu_avg_1min real3,
cpu_avg_5min real3,
cpu_avg_15min real3,
mem_free bigint,
mem_used bigint,
rssi bigint
)
数据写入程序由C#编写,在博客末尾提供完整的程序。
最终写入3000万条数据耗时:423秒,平均每秒写入7万条数据。
四 数据库查询测试项
1.查询数据总条数
select count(*) as cnt from readings
2.查询某设备的所有数据
select * from readings
where devid = 10327
limit 10000
3. 查询某设备最新的1000条数据,按时间戳倒序
select * from readings
where devid = 10027
order by tstamp desc
limit 1000
4. 查询某设备从指定时间开始的1000条数据
select * from readings
where devid = 10213 and tstamp >= '2016-11-15 14:0:0'
limit 1000
5. 查询某设备满足一定条件的数据条数
select count(*) as cnt from readings
where devid = 10523 and cpu_avg_1min > 30
6. 查询某设备在2016-11-16按小时聚合的统计
select tstamp, avg(battery_temperature) as avg_battery
from readings
where devid = 12330 and tstamp >= '2016-11-16'
group by tstamp 1h
limit 24
7. 查询所有设备最新的数据
select devid,
last(tstamp) as lst_tm,
last(battery_level) as lst_battery_level,
last(battery_status) as lst_battery_status,
last(battery_temperature) as lst_battery_temperature,
last(cpu_avg_1min) as lst_cpu_avg_1min,
last(cpu_avg_5min) as lst_cpu_avg_5min,
last(cpu_avg_15min) as lst_cpu_avg_15min,
last(mem_free) as lst_mem_free,
last(mem_used) as lst_mem_used,
last(rssi) as lst_rssi
from readings
group by devid
limit 3000
五 普通文件查询测试
普通文件查询分为所有数据都在内存中和所有数据都在固态盘上两种情形的对比。松果时序数据库中普通文件的内存缓存完全有我们管理,并且读写磁盘时没有使用文件系统缓存,故重启服务即清空所有内存缓存,查询一次所有数据(select count(*) as cnt from readings)就将数据都加载到了内存中(这里的测试数据小于内存缓存)。
测试结果如下:
六 压缩文件查询测试
压缩文件仅测试数据在磁盘上的情况,为了消除缓存的影响,每次测试前都重启机器。
在普通文件测试后,将松果时序数据库停止,并将配置文件中设置有效插入时间为1,确保compressFlag=true,重启松果时序数据库,一般情况下40分钟后会启动将普通文件转换为压缩文件,耐心等待一下即可。可以通过查询sys_datafile系统表确认数据文件的类型。在此步骤下,配置文件的内容如下所示:
测试结果如下:
七 占用磁盘对比
数据写入后普通文件大小为 1.37GB,各文件如下所示:
将数据文件转换为压缩文件后,文件总大小为 403MB,各文件如下所示:
八 总结
从上面的测试结果来看,松果时序数据库查询总数的性能较低,这是由于内部并没有对查询总条数做特殊的优化,也即:查询总条数需要将所有数据扫描一遍,查询性能会受到数据量的影响。在实际应用中不要查询所有数据的总条数,需要时可以添加条件比如时间范围,设备ID(注:除了devid和tstamp的条件,其他字段的条件不会对查询性能产生影响)。
若仅仅获取某个设备、某个时间段内的数据,不管是原始数据还是统计数据,松果时序数据库都会有较高的性能(当然也受数据文件类型,是否已经在内存中的影响)这也是物联网场景常用的操作。
Real系列数据类型有着极高的压缩性能。对于需要长期存储数据的场景,松果时序数据库提供了极高的数据压缩,大大降低了存储空间的占用。
松果时序数据库的目标是为客户提供简单、易用、高性能的产品,若需要了解更多的信息请关注我们的官网及源码仓库。
官网:http://www.pinusdb.cn
码云仓库:https://gitee.com/pinusdb/pinusdb
GitHub仓库:https://github.com/pinusdb/pinusdb
附:C#数据写入程序
class Program
{
static string connStr = "server=127.0.0.1;port=8105;username=sa;password=pin123";
static string tabName = "readings";
static string filePath = ".\\devices_big_readings.csv";
static void Main(string[] args)
{
CreateDevice();
InsertData();
Console.WriteLine("输入回车退出");
Console.ReadLine();
}
static void CreateDevice()
{
DataTable dtDev = new DataTable("sys_dev");
dtDev.Columns.Add(new DataColumn("tabname", typeof(string)));
dtDev.Columns.Add(new DataColumn("devid", typeof(long)));
using (PDBConnection conn = new PDBConnection(connStr))
{
conn.Open();
PDBCommand cmd = conn.CreateCommand();
for(long devId = 10000; devId < 13000; devId++)
{
DataRow drDev = dtDev.NewRow();
drDev[0] = tabName;
drDev[1] = devId;
dtDev.Rows.Add(drDev);
if (dtDev.Rows.Count == 1000)
{
PDBErrorCode retCode = cmd.ExecuteInsert(dtDev);
if (retCode != PDBErrorCode.PdbE_OK)
{
throw new Exception("添加设备失败");
}
dtDev.Rows.Clear();
}
}
}
}
static void InsertData()
{
long insertCnt = 0;
Stopwatch sw = new Stopwatch();
sw.Start();
DataTable dtReadings = new DataTable(tabName);
dtReadings.Columns.Add(new DataColumn("devid", typeof(long)));
dtReadings.Columns.Add(new DataColumn("tstamp", typeof(DateTime)));
dtReadings.Columns.Add(new DataColumn("battery_level", typeof(long)));
dtReadings.Columns.Add(new DataColumn("battery_status", typeof(string)));
dtReadings.Columns.Add(new DataColumn("battery_temperature", typeof(double)));
dtReadings.Columns.Add(new DataColumn("cpu_avg_1min", typeof(double)));
dtReadings.Columns.Add(new DataColumn("cpu_avg_5min", typeof(double)));
dtReadings.Columns.Add(new DataColumn("cpu_avg_15min", typeof(double)));
dtReadings.Columns.Add(new DataColumn("mem_free", typeof(long)));
dtReadings.Columns.Add(new DataColumn("mem_used", typeof(long)));
dtReadings.Columns.Add(new DataColumn("rssi", typeof(long)));
using (PDBConnection conn = new PDBConnection(connStr))
{
conn.Open();
PDBCommand cmd = conn.CreateCommand();
StreamReader dataReader = new StreamReader(filePath);
string lineStr = null;
while((lineStr = dataReader.ReadLine()) != null)
{
string[] partArr = lineStr.Split(',');
if (partArr.Count() != 13)
continue;
DataRow drReadings = dtReadings.NewRow();
drReadings[0] = 10000 + Convert.ToInt64(partArr[1].Substring(4));
drReadings[1] = Convert.ToDateTime(partArr[0]);
drReadings[2] = Convert.ToInt64(partArr[2]);
drReadings[3] = partArr[3];
drReadings[4] = Convert.ToDouble(partArr[4]);
drReadings[5] = Convert.ToDouble(partArr[6]);
drReadings[6] = Convert.ToDouble(partArr[7]);
drReadings[7] = Convert.ToDouble(partArr[8]);
drReadings[8] = Convert.ToInt64(partArr[9]);
drReadings[9] = Convert.ToInt64(partArr[10]);
drReadings[10] = Convert.ToInt64(partArr[11]);
dtReadings.Rows.Add(drReadings);
if (dtReadings.Rows.Count == 1000)
{
PDBErrorCode retCode = cmd.ExecuteInsert(dtReadings);
if (retCode != PDBErrorCode.PdbE_OK)
throw new Exception("插入数据失败");
insertCnt += dtReadings.Rows.Count;
dtReadings.Clear();
if (insertCnt % 100000 == 0)
{
Console.WriteLine("{0}: 已插入数据: {1}", DateTime.Now.ToString(), insertCnt);
}
}
}
sw.Stop();
Console.WriteLine("插入{0}条数据, 耗时:{1}毫秒, 平均每秒插入:{2}条", insertCnt, sw.ElapsedMilliseconds, (insertCnt / (sw.ElapsedMilliseconds / 1000)));
}
}
}