SENSORO 基于 TDengine 助力基层政府打造数字化应用标杆

小T导读:SENSORO(北京升哲科技有限公司)是一家领先的物联网与人工智能独角兽企业。作为城市级数据服务提供商,公司在新一代信息技术领域拥有核心研发能力,在国内首次实现物联网与人工智能领域端到端、一体化的技术与产品能力,包含自研物联网通信芯片、通信基站、智能感知终端和智能视觉终端及核心数据平台等。SENSORO 面向城市基础设施与核心要素提供全域数字化服务方案,通过将多项核心自研关键性技术深入应用到智慧城市、乡村振兴、区域治理、社会民生等领域,打造物联网与人工智能应用的数字应用标杆,赋能我国城乡数字经济的高质量发展。

建立城市级传感器网络所涉及的传感器种类十分多样,由此产生的数据量也十分庞大, 如果只是使用 MySQL、PostgreSQL 等 OLTP 系统进行数据的简单存储,不仅会产生很多问题,而且其水平扩展能力也有限,同时也因为没有专门针对物联网数据进行优化而缺乏足够的压缩效果,数据存储成本很高。

在系统开发初期,结合之前的经验我们先是选择了 Apache Druid 作为存储传感数据的数据库,然而在使用过程中却遇到了各种各样的问题,这使得我们将目光转移到了 TDengine 这款时序数据库(Time-Series Database)。事实上,在 TDengine 开源之初我们就注意到了这个新兴的时序数据库,阅读当时发布的白皮书与性能测试报告时惊艳感由衷而生,随即联络到了涛思的同学们,进行了更深入的交流与测试。

但因为平台涉及的特殊数据模型,合作便一直搁置了下来。主要问题在于数据有 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 的系统架构
基于 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 亿条记录的结果。这里要注意一点,由于数据很多为随机生成,数据压缩率一般会比真实情况要差。

  • 资源对比:
资源对比 TDengine Database
  • 响应时间对比:
  1. 随机单设备原始数据查询
    1. 查询结果集100条
    2. 重复1000次查询,每次查询设备随机指定
    3. 查询时间区间分别为:1天、7天、1月,
    4. 统计查询耗时的最大值、最小值、平均值
    5. SELECT * FROM device_${random} LIMIT 100
随机单设备原始数据查询 TDengine Database
  1. 随机单设备聚合查询
    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})
随机单设备聚合查询 TDengine Database
  1. 随机多设备聚合查询
    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 Database

可以看到,TDengine 的空间占用只有 Druid 的 60%(没有计算 Druid 使用的 Deep Storage)。针对单一设备的查询与聚和的响应时间比 Druid 有倍数的提升,尤其时间跨度较久时差距更明显(在十倍以上),同时 Druid 的响应时间方差也较大。然而针对多子表的聚合操作,TDengine 与 Druid 的区别便不再明显,可以说是各有优劣。

总之,TDengine 与 Druid 在物联网数据方面的对比,前者的性能、资源使用方面均有较大领先。再结合 TDengine 安装部署配置上的便利性(我们会涉及到一些私有化应用的部署场景,这点对我们来说非常重要),及相较于 Apache 社区其所提供的更可靠与及时的商业服务,我们最终决定将传感数据迁移到 TDengine中。

三、迁移后的系统

  • 建表与迁移

因为我们系统内接入的设备种类非常多,所以一开始数据存储便以大宽表的方式存储: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 Database
基于TDengine后的系统架构

当然期间也遇到了一些小问题,主要就是在根据设备建表时,某些前缀加设备唯一标识构成表名,但设备唯一标识里面可能会包含减号”-“这种特殊字符。对于当时的 TDengine Database 版本来说,这种特殊或保留字符是无法作为表名或列名的,所以额外处理了一下。此外列名无法区分大小写也使得我们原本“fooBar”这种驼峰方式的命名需要修改成“foo_bar”这种下划线分隔。不过 TDengine 2.3.0.0 之后支持了转义字符“`”后,这些问题就都得到了解决。

  • 迁移后效果

迁移后,TDengine 为我们系统里的各式各样的传感器提供了统一的数据存储服务,通过中间数据层的封装,我们上层的业务基本无需修改便可以顺利地迁移过来。相比于 Druid 需要部署各种各样的 Server,TDengine 仅需要部署 DNode 即可,也不再需要部署 PG、ZK、Ceph 等外部依赖。

迁移后的应用接口响应时间 P99 也从 560 毫秒左右降低到 130 毫秒(涉及多次内部 RPC 调用与 Database 的查询并不单纯表示 TDengine 查询响应时间):

某历史数据查询接口响应时间 TDengine Database
某历史数据查询接口响应时间
某数据聚合接口响应时间 TDengine Database
某数据聚合接口响应时间

对于开发人员来说,TDengine 让我们不需要再花费太多时间与精力去研究查询怎么样更高效(只要不直接使用数据列做过滤条件并指定合理的查询时间段后,大部分查询都能得到满意的响应时间),可以更多地聚焦于业务功能实现上。同时我们的运维同学们也得以从 Druid 的各个复杂模块中解脱出来,在操作任何中间件时都不需要再对 Druid 的情况进行确认。

且值得一提的是,在实际业务环境中,以上面描述的方式创建多列的超级表,虽然会存在大量的空列,但得益于 TDengine 的优化,能达到恐怖的 0.01 的压缩率,简单计算下来大约需要 3.67GB 每亿条。另外一张超级表(约 25 列数据列)针对传感器数据进行单独建模(不会存在空列的情况),压缩率也有 0.2,计算一下空间使用约合 3.8GB 每亿条。这样看来使用宽表这种存储方式结合 TDengine 的强大压缩能力也不会带来很多额外的硬件成本开销,但却能显著的降低我们的维护成本。

四、未来规划

目前我们基于 TDengine 主要还是存储传感器设备上传的数据,后续也计划将基于传感数据分析出的事件数据迁移过来,甚至还打算将 AI 识别算法分析出的结构化数据也存储到 TDengine 中。总之,经历了此次合作,我们会把 TDengine 作为数据中台里重要的一种存储引擎使用,而非简单地存储传感器数据。未来,相信在涛思同学们的支持下,我们能为客户提供更加优质的服务,打造物联网与人工智能应用的数字标杆。

作者简介

段雪林,北京升哲高级后端开发工程师,主要负责升哲灵思物联网中台的设计开发工作。