作者:段雪林
小T导读:SENSORO(北京升哲科技有限公司)是一家领先的物联网与人工智能独角兽企业。作为城市级数据服务提供商,公司在新一代信息技术领域拥有核心研发能力,在国内首次实现物联网与人工智能领域端到端、一体化的技术与产品能力,包含自研物联网通信芯片、通信基站、智能感知终端和智能视觉终端及核心数据平台等。SENSORO 面向城市基础设施与核心要素提供全域数字化服务方案,通过将多项核心自研关键性技术深入应用到智慧城市、乡村振兴、区域治理、社会民生等领域,打造物联网与人工智能应用的数字应用标杆,赋能我国城乡数字经济的高质量发展。
建立城市级传感器网络所涉及的传感器种类十分多样,由此产生的数据量也十分庞大, 如果只是使用MySQL、PostgreSQL等OLTP系统进行数据的简单存储,不仅会产生很多问题,而且其水平扩展能力也有限,同时也因为没有专门针对物联网数据进行优化而缺乏足够的压缩效果,数据存储成本很高。
在系统开发初期,结合之前的经验我们先是选择了Apache Druid作为存储传感数据的数据库,然而在使用过程中却遇到了各种各样的问题,这使得我们将目光转移到了TDengine这款时序数据库(Time-Series Database)。事实上,在TDengine Database开源之初我们就注意到了这个新兴的时序数据库,阅读当时发布的白皮书与性能测试报告时惊艳感由衷而生,随即联络到了涛思的同学们,进行了更深入的交流与测试。
但因为平台涉及的特殊数据模型,合作便一直搁置了下来。主要问题在于数据有A、B两个维度且是多对多关系还会随时间变化,基于A创建子表(此时无法将B设置成tag列)就无法通过B进行聚合查询,还需要花费较大的时间与精力改造成TDengine特有的超级表结构。之后TDengine也经过了多个版本迭代,支持了join查询,而我们的数据模型也发生了变化,迁移到TDengine时不再需要做出很多的系统模块改动。
一、基于Apache Druid现存系统的问题
基于Apache Druid,系统最大的问题就是维护成本了。Druid划分了Coordinator、Overlord、Broker、Router、Historical、MiddleManager六个进程,要实现完整的集群功能,其还需要Deep Storage (支持S3和HDFS),Metadata Storage(典型如MySQL、PGSQL),以及为实现服务发现与选主功能而需要的ZooKeeper,由此也可以看出Druid是一套极为复杂的系统。
同时,Druid对外部的各种依赖也导致运维同学在处理一些问题时,会直接或间接地影响到它的运行,比如我们将S3的AccessKey进行规范化处理——由以前的全局通用改成某个bucket唯一,或者将PGPool升级,都会影响到Druid。而且Druid针对每一个进程和外部依赖都有厚厚的几页配置项,且从JVM自身来看,不同进程、配置、MaxDirectMemorySize都会严重影响写入查询性能。如果你要从官方文档的配置页面从顶划到底,可能会把手指划抽筋。
基于Apache Druid 的系统架构(Druid每个进程都单独的部署并有不同的配置)
为了节省存储成本,我们在部署Druid集群时对于Historical节点采用了多种不同的机器配置,在近期数据的处理上,机器配备SSD硬盘并设置较多副本数。这导致数量最多的Data Server节点,有一些不能与Middle Manager共享,同时不同的节点因为配备了不同核数CPU与内存,对应的JVM配置和其他线程池配置也不同,进一步加大了运维成本。
另外,由于Druid的数据模型分为Primary timestamp、Dimensions、Metrics,而Metrics列只能在启用Druid的Rollup时才会存在,而Rollup意味着写入时聚合且数据会有一定程度的丢失。这种情况下,想把每行数据都原原本本地记录下来,只能把数据全都记录在Dimensions列,不使用Metrics,而这也会影响数据压缩以及某些场景的聚合查询性能。
此外还有一些问题如Druid的SQL编译性能问题、原生查询复杂的嵌套结构等在此便不再一一列举,总之基于上述问题我们决定再次详细测试一下TDengine Database。
二、与Druid的对比
导入相同的两份数据到Druid和TDengine中,以下为在三节点(8c16g)环境下,100万个传感设备、每个传感设备是40列(6个字符串数据列、30个double数据列以及4个字符串tag列),总计5.5亿条记录的结果。这里要注意一点,由于数据很多为随机生成,数据压缩率一般会比真实情况要差。
- 资源对比:
- 响应时间对比:
1. 随机单设备原始数据查询
1) 查询结果集100条
2) 重复1000次查询,每次查询设备随机指定
3) 查询时间区间分别为:1天、7天、1月,
4) 统计查询耗时的最大值、最小值、平均值
5) SELECT * FROM device_${random} LIMIT 100
2. 随机单设备聚合查询
1) 聚合计算某列的时间间隔的平均值
2) 重复1000次查询,每次查询设备随机指定
3) 查询时间区间分别为:1天、7天、7天、1月,对应聚合时间为1小时、1小时、7天,7天。
4) 统计查询耗时的最大值、最小值、平均值
5) SELECT AVG(col_1) FROM device_${random} WHERE ts >= ${tStart} and ts < ${tEnd} INTERVAL(${timeslot})
3. 随机多设备聚合查询
1) 聚合计算某列的时间间隔的总和
2) 重复1000次查询,每次查询设备约10000个
3) 查询时间区间分别为:1天、7天、7天、1月,对应聚合时间为1小时、1小时、7天,7天。
4) 统计查询耗时的最大值、最小值、平均值
5) SELECT SUM(col_1) FROM stable WHERE ts >= ${tStart} and ts < ${tEnd} AND device_id in (${deviceId_array}) INTERVAL(${timeslot})
可以看到,TDengine的空间占用只有Druid的60%(没有计算Druid使用的Deep storage)。 针对单一设备的查询与聚和的响应时间比Druid有倍数的提升,尤其时间跨度较久时差距更明显(在十倍以上),同时Druid的响应时间方差也较大。然而针对多子表的聚合操作,TDengine与Druid的区别便不再明显,可以说是各有优劣。
总之,TDengine与Druid在物联网数据方面的对比,前者的性能、资源使用方面均有较大领先。再结合TDengine安装部署配置上的便利性(我们会涉及到一些私有化应用的部署场景,这点对我们来说非常重要),及相较于Apache社区其所提供的更可靠与及时的商业服务,我们最终决定将传感数据迁移到TDengine Database中。
三、迁移后的系统
- 建表与迁移
因为我们系统内接入的设备种类非常多,所以一开始数据存储便以大宽表的方式存储:50列double类型、20列binary类型、10列bool以及额外的几列通用列,同时还额外维护了一份记录了每列实际列名的映射表。这种存储模式在基于Druid的系统中便已经实现了,在TDengine中我们也创建了同样结构的超级表,列名如: number_col1, number_col2, ..., number_col50, str_col1, str_col2, ..., str_col10
。
在原本的数据写入服务中,会将{"foo": 100, "bar": "foo"}
转换成 {"number_col1": 100, "str_col1": "foo"}
,同时记录一份 [foo=> number_col1, bar=>str_col1]
的映射关系(每一型号的设备共用相同的映射),然后将处理后的数据写入Kafka集群中。
现在要将数据写入到TDengine中,也只需要基于原本要写入Kafka的数据来生成对应的insert SQL,再通过TAOSC写入TDengine即可,且在数据查询时也会自动从映射关系中读取对应的真实列名返回给调用方。这样对上层应用来说,输入输出的数据保证了统一且无需变动,同时即便我们系统频繁的增加新的设备类型,基本上也不再需要手动创建新的超级表。
基于TDengine后的系统架构
当然期间也遇到了一些小问题,主要就是在根据设备建表时,某些前缀加设备唯一标识构成表名,但设备唯一标识里面可能会包含减号”-“这种特殊字符。对于当时的TDengine版本来说,这种特殊或保留字符是无法作为表名或列名的,所以额外处理了一下。此外列名无法区分大小写也使得我们原本“fooBar”这种驼峰方式的命名需要修改成“foo_bar”这种下划线分隔。不过TDengine 2.3.0.0之后支持了转义字符“`”后,这些问题就都得到了解决。
- 迁移后效果
迁移后,TDengine Database为我们系统里的各式各样的传感器提供了统一的数据存储服务,通过中间数据层的封装,我们上层的业务基本无需修改便可以顺利地迁移过来。相比于Druid需要部署各种各样的Server,TDengine仅需要部署DNode即可,也不再需要部署PG、ZK、Ceph等外部依赖。
迁移后的应用接口响应时间P99也从560毫秒左右降低到130毫秒(涉及多次内部RPC调用与Database的查询并不单纯表示TDengine查询响应时间):
某历史数据查询接口响应时间
某数据聚合接口响应时间
对于开发人员来说,TDengine让我们不需要再花费太多时间与精力去研究查询怎么样更高效(只要不直接使用数据列做过滤条件并指定合理的查询时间段后,大部分查询都能得到满意的响应时间),可以更多地聚焦于业务功能实现上。同时我们的运维同学们也得以从Druid的各个复杂模块中解脱出来,在操作任何中间件时都不需要再对Druid的情况进行确认。
且值得一提的是,在实际业务环境中,以上面描述的方式创建多列的超级表,虽然会存在大量的空列,但得益于TDengine的优化,能达到恐怖的0.01的压缩率,简单计算下来大约需要3.67GB每亿条。另外一张超级表(约25列数据列)针对传感器数据进行单独建模(不会存在空列的情况),压缩率也有0.2,计算一下空间使用约合3.8GB每亿条。 这样看来使用宽表这种存储方式结合TDengine的强大压缩能力也不会带来很多额外的硬件成本开销,但却能显著的降低我们的维护成本。
四、未来规划
目前我们基于TDengine Database主要还是存储传感器设备上传的数据,后续也计划将基于传感数据分析出的事件数据迁移过来,甚至还打算将AI识别算法分析出的结构化数据也存储到TDengine中。 总之,经历了此次合作,我们会把TDengine作为数据中台里重要的一种存储引擎使用,而非简单地存储传感器数据。未来,相信在涛思同学们的支持下,我们能为客户提供更加优质的服务,打造物联网与人工智能应用的数字标杆。
作者简介
段雪林,北京升哲高级后端开发工程师,主要负责升哲灵思物联网中台的设计开发工作。
想了解更多TDengine的具体细节,欢迎大家在GitHub上查看相关源代码。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。