Skywalking IoTDB存储插件设计

本博客基于结项时的项目报告,写于2021年9月中下旬,内容稍有落后。代码Merge前发布的英文博客中包含了更详细易懂的例子,如有需要请参考。

该项目来自于开源软件供应链点亮计划 - 暑期2021的Apache IoTDB - Apache SkyWalking适配器项目。插件的设计和开发工作都得到了IoTDB的黄向东老师(@jixuan1989)和SkyWalking的吴晟老师(@wu-sheng)的指导。感谢他们的指导和社区的帮助。目前项目已提交至SkyWalking社区(IoTDB storage plugin #7766),正在接受社区评审。

实现思路

项目目标是为SkyWalking开发一个使用IoTDB进行相关数据读写的适配器。可以参考SkyWalking目前已有的采用InfluxDB、H2和Elasticsearch的适配器。SkyWalking给出了存储拓展的开发指南,参考链接。此外,需要使用SkyWalking和InfluxDB的调试过程,以便充分了解SkyWalking的接口和使用方式。

IoTDB建议使用其提供的原生客户端封装: Session或Session Pool与IoTDB服务端进行交互。

开发平台:Win10,IoTDB版本:0.12.3

相关概念

SkyWalking的存储模型

SkyWalking 8.0+的存储模型大致分为4类:Record,Metrics,NoneStream,ManagementData。它们都实现了StorageData接口。StorageData接口必须实现id()方法,下面分别介绍4类存储模型:

  • Record:Record大部分是原始日志数据或任务记录。这些数据需要持久化,无需进一步分析。所有Record类的模型均具备time_bucket字段,用于记录当前Record所在的时间窗口。具体例子有:SegmentRecord,AlarmRecord,BrowserErrorLogs,LogRecord,ProfileTaskLogRecord, ProfileThreadSnapshotRecord,TopN。

    • SegmentRecord:Trace Segment明细记录模型。由skywalking-trace-receiver-plugin插件接收并解析SkyWalking Agent发送来的链路数据后得到TraceSegment。
    • AlarmRecord:报警明细记录模型。在指标触发报警规则时,会产生对应的报警明细数据模型。
    • TopN:TopN是采样模型,具备statement字段(用于描述采样数据的关键信息),latency字段(用于记录采样数据的延迟),
      trace_id字段(用于描述采样数据的关联分布式链路ID),service_id字段(用于记录服务ID)。目前采样模型默认只有TopNDatabaseStatement。
      • TopNDatabaseStatement:按照延迟排序的DB采样记录。
  • Metrics:Metrics表示统计数据,是通过OAL脚本或硬编码对源(Source)数据进行聚合分析后生成的存储模型。它的生命周期由TTL(生存时间)控制。所有Metrics类的模型均具备time_bucket和entity_id字段。例如:NetworkAddressAlias,Event,InstanceTraffic,EndpointTraffic, ServiceTraffic,EndpointRelationServerSideMetrics,ServiceInstanceRelationServerSideMetrics, ServiceInstanceRelationClientSideMetrics,ServiceRelationServerSideMetrics,ServiceRelationClientSideMetrics

  • NoneStream:NoneStream基于Record,支持time_bucket转换为TTL。例如:ProfileTaskRecord

  • ManagementData:UI模板管理相关的数据,默认只有一个UITemplate实现类

部分参考《Apache SkyWalking实战》第9章:SkyWalking OAP Server存储模型

IoTDB的数据模型

简单来说,可以用树结构来认识IoTDB的数据模型。如果按照层级划分,从高到低依次为:Storage Group -> (LayerName) -> Device -> Measurement。从最上层到其下某一层称为一条路径(Path),最上层是Storage Group,倒数第二层是Device,倒数第一层是Measurement,中间可以有很多层,每一层姑且称之为LayerName。更多信息请参考IoTDB v0.12.x的官方数据模型介绍

值得注意的是,每个Storage Group需要一个线程,所以Storage Group过多会导致存储性能下降。此外,LayerName的值存储在内存中,而Measurement的值及其下的数据存储在硬盘中。

SkyWalking的IoTDB-adapter存储方案

概念划分

SkyWalking的每个存储模型可以认为是一个Model,Model中包含了多个Column,每个Column中具备ColumnName和ColumnType属性,分别表示Column的名字和类型,每个名为ColumnName的Column下存储多个数据类型为ColumnType的Value。从关系型数据库的角度来看的话,Model即是关系表,Column即是关系表中的字段。

方案一:类似关系型数据库的存储方案(无法实现)

将SkyWalking的所有存储模型都写入IoTDB的一个存储组中,例如root.skywalking存储组。Model对应Device,Column对应Measurement。即SkyWalking的Database -> Model -> Column对应到IoTDB的Storage Group -> Device -> Measurement。该方案的IoTDB存储路径只有4层:root.skywalking.ModelName.ColumnName。该方案的优点是逻辑清晰,实现难度较低,但由于数据都存储在硬盘上,查询效率相对较差。

验证结果:该方案无法实现
原因:部分存储接口需要实现group by entity_id的查询功能,但IoTDB只支持group by time。需要采用方案二并通过group by level来实现。

方案二:引入索引的存储方案

由于IoTDB的每个LayerName存储于内存中,可以将其认为是一种索引,可以充分利用LayerName的这个特性提高IoTDB的查询性能。

依然将SkyWalking的所有存储模型都写入IoTDB的一个存储组中,例如root.skywalking存储组,它会占据path的前两层。Model对应一个LayerName,它会占据path的第三层,相当于root.skywalking.model_name。需要索引的Column也对应于LayerName,不过LayerName并不存储ColumnName,而是存储对应的Value,相当于需要索引的一个Column的不同Value存储在同一分支下的同一层。不需要索引的Column依然对应Measurement。最终来自同一个Model的数据被分散到多个Device中。

SkyWalking有其自己的索引要求,但它们并不适用于IoTDB。考虑到查询频率和其它存储插件的实现,我们选择id, entity_id, node_type, service_id, service_group, trace_id作为索引,并固定它们的出现顺序。如果不这样做,我们就不能将它们的value映射到对应的column,因为该方案丢失了需要索引的ColumnName并且直接将它们的value存储在path中。

SkyWalking数据模型到IoTDB数据模型的映射关系如下:

SkyWalkingIoTDB
DatabaseStorage Group(1st and 2nd layer of the path)
ModelLayerName(3rd layer of the path)
Indexed Columnstored in memory through hard-code
Indexed Column ValueLayerName(after 3rd layer of the path)
Non-indexed ColumnMeasurement
Non-indexed Valuethe value of Measurement

该方案的IoTDB存储路径长度是不定的,索引的Column越多,路径的长度越长。一个较一般的例子如下:

当前有model1(column11, column12),model2(column21, column22, column23),model3(column31),下划线说明该字段需要索引。

  • 需要具备索引的Model:
    • root.skywalking.model1_name.column11_value.column12_name
    • root.skywalking.model2_name.column21_value.column22_value.column23_name
  • 不需要具备索引的Model:
    • root.skywalking.model3_name.column31_Name

插入:

-- 插入model1
insert into root.skywalking.model1_name.column11_value values(timestamp, column12_value);
-- 插入model2
insert into root.skywalking.model2_name.column21_value.column22_value values(timestamp, column23_value);
-- 插入model3
insert into root.skywalking.model3_name value(timestamp, column31_value);

查询:

-- 查找model1的所有数据
select * from root.skywalking.model1_name align by device;
-- 查找model2中column22_value="test"的数据
select * from root.skywalking.model2_name.*.test align by device;
-- 按照column21分组统计model2中column23的和
select sum(column23) from root.skywalking.model2_name.*.* group by level = 3;

已确定采用索引的字段和它们的顺序

id
entity_id
node_type
service_group
service_id
trace_id

此外,可以将SkyWalking Model中的time_bucket转换为IoTDB的时间戳timestamp后进行存储,而无需另行存储time_bucket

该方案的优点是实现了索引功能,类似InfluxDB的tag,但逻辑较复杂,实现难度较大。另一方面还需进一步确定哪些Column需要作为索引列,这一点可以参考Elasticsearch(StorageEsInstaller),InfluxDB(TableMetaInfo),MySQL(MySQLTableInstaller)的实现,以及ModelColumn的isStorageOnly属性。

该方案将部分数据通过LayerName存储在内存中,在海量数据的情况下可能会导致内存开销较大。此外,由于同一个Model的数据被分散到了多个Device中,所以查询往往需要跨多个Device进行查询。但在这一方面,IoTDB对于跨Device的聚合查询、排序查询、分页查询的支持还不够完善,在一些情况下需要使用暴力法将所有符合条件的数据都查出来,然后再自行实现聚合、排序、分页的功能。具体描述可参考下文在IoTDB社区提交的Issue和Discussion。

方案的性能测试

参考来自开源软件供应链点亮计划 - 暑期2021的兼容InfluxDB协议或客户端项目的设计文档,可以看到,使用索引的情况和不使用索引的情况在查询时间上有数倍的差距。

SkyWalking-IoTDB适配器的设置参数

  1. host,IoTDB主机IP,默认127.0.0.1
  2. rpcPort,IoTDB监听端口,默认6667
  3. username,用户名,默认root
  4. password,密码,默认root
  5. storageGroup,存储组名称,默认root.skywalking
  6. sessionPoolSize,SessionPool大小,默认16
  7. fetchTaskLogMaxSize,在一次请求中获取的TaskLog数量的最大值,默认1000

方案的实施

首先,我的开发遵循SkyWalking官方的官方指南:Storage extension development guide.

当oap启动时,IoTDB存储插件将model的元信息通过IoTDBTableMetaInfo保存在内存中,同时转换将它们的结构以满足我们的设计。对于每个model,用List<String> indexes来记录它的需要索引的ColumnName,用Map<String, TSDataType> columnAndTypeMap来记录其它的ColumnName以及它们的TSDataType。

当插入数据时,IoTDB存储插件在IoTDBMetricsDAO, IoTDBRecordDAO, IoTDBNoneStreamDAOIoTDBManagementDAO中设置或者转换timestamp,然后在IoTDBBatchDAO中执行写入。我们使用IoTDBInsertRequest来封装所有的插入和更新请求。它会使用来自IoTDBTableMetaInfo的信息构建index和indexValue的映射,以及measurement和measurementType,measurementValue的映射。同时它会转换那些非法值。此外,因为一些Record Model有可变化数量的tag,所以我们有时会在IoTDBRecordDAO中修改IoTDBInsertRequest。这些tag被处理为measurement

当查询请求到来时,IoTDB存储插件会在QueryDAOIoTDBMetricsDAO执行它们。如上所述,因为我们的设计使得来自同一个model的数据分散在多个Device中,并且IoTDB对于跨Device的聚合、排序、分页查询支持不够好,所以有些查询不得不使用暴力法。所以查询效率可能不高。

遇到的问题及解决方案

问题1:SkyWalking部分存储接口要求order by查询,但IoTDB仅支持order by time。这本来可以使用选择函数top_kbottom_k来过滤数据。但在使用索引方案的情况下,由于数据点分散在多个device中,top_kbottom_k函数无法过滤数据,也无法使用类似group by level的合并device的查询方法,具体的描述可参考:Discussion #3888, Issue #3905

  • 解决方案:目前除暴力法全部查询出来再排序以外暂无其他解决方案。

问题2:SkyWalking部分存储接口要求group by entity_id的分组聚合查询,但IoTDB仅支持group by timegroup by level

  • 解决方案:采用方案二,并将entity_id作为LayerName存储,通过group by level实现分组查询。

问题3:在使用索引方案的情况下,由于数据点分散在多个device中,聚合函数的使用必须要通过group by level才能正确统计数据。但在此情况下,group by levelwhere同时使用得不到预期的结果。应该要加上align by device语义才行,但加上以后就会引起IllegalPathException,推测是因为IoTDB不能同时支持group by levelalign by device的语义。具体描述可以参考Discussion #3907

  • 解决方案:运用align by devicewhere过滤查询所有数据后自行实现聚合函数的功能。

问题4:SkyWalking的存储接口要求模糊查询,但IoTDB使用like的模糊查询并不支持跨device查询,不适用于方案二。此外,IoTDB的字符串函数string_contains只能返回true/false,并不会对数据进行过滤。具体描述可以参考:Issue #3945

  • 解决方案:最初使用select *, string_contains获得查询结果后再根据true/false循环过滤。后来@ijihang提交的PR#3953 ,使like支持跨device查询和align by device。所以该问题可以直接使用类似MySQL的模糊查询即可,再次感谢。

问题5:SkyWalking的存储接口要求模糊查询和过滤查询。由于采用了索引方案,所以需要跨device查询,但使用string_contains函数的过滤查询对多个device之间采用了and的语义,无法正确过滤数据,如果是or的语义应该就可以正确过滤数据了。同时string_contains也不支持使用align by device。以上这种情况仅支持对time的过滤。

  • 解决方案:最初使用select *, string_contains from root.skywalking.xxx.* where time > ?获得结果后自行对其他条件过滤。后来采用类似问题4的解决方案,直接使用类似MySQL的模糊查询和过滤查询即可。

总结

当前的存储方案还存在使用暴力法进行查询的情况,存储插件的性能可能存在问题,目前缺乏测试。暂时还没有好的方法可以解决,要等后续IoTDB针对这类场景增加不同语义的关键字或者完善现有的查询语义才行。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值