Skywalking IoTDB存储插件设计说明

Posted Timcross-WEI

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Skywalking IoTDB存储插件设计说明相关的知识,希望对你有一定的参考价值。

该项目来自于开源软件供应链点亮计划 - 暑期2021的Apache IoTDB - Apache Skywalking适配器项目。目前已提交至Skywalking社区(IoTDB storage plugin #7766),正在接受社区评审。

实现思路

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

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

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

相关概念

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

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

Skywalking的IoTDB-adapter存储方案

概念划分

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

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

将Skywalking的所有存储模型都写入IoTDB的一个存储组中,例如root.skywalking存储组。Model对应Device,Column对应Timeseries。即Skywalking的“Database -> Model -> Column”对应到IoTDB的“Storage Group -> Device -> Timeseries”。该方案的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存储组。Model对应一个LayerName,需要索引的Column也对应于LayerName,不过LayerName并不存储ColumnName,而是存储对应的Value,相当于需要索引的一个Column的不同Value存储在同一分支下的同一层。不需要索引的Column依然对应Timeseries,即路径的最后一层。最终将导致同一个Model的数据分散到多个Device中。

由于该方案丢失了需要索引的ColumnName,所以需要通过硬编码记录需要索引的ColumnName及ColumnType。此外为了避免存储的混乱,还需要保证一个Model下多个索引Column的顺序。计划通过硬编码存储Model需要索引的Column的存储顺序。

该方案的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_name);

查询:

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

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

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

已确定采用索引的字段

id
entity_id
node_type
service_group
service_id
trace_id

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

方案的性能测试

参考来自开源软件供应链点亮计划 - 暑期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

遇到的问题及解决方案

问题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的模糊查询和过滤查询即可。

总结

当前的存储方案还存在使用暴力法进行查询的情况,即使通过Skywalking社区的审查,性能也是不足的,具有数据传输开销大、系统处理资源占用大的问题。暂时还没有好的方法可以解决,要等后续IoTDB针对这类场景增加不同语义的关键字或者完善现有的查询语义才行。

以上是关于Skywalking IoTDB存储插件设计说明的主要内容,如果未能解决你的问题,请参考以下文章

skywalking插件工作原理剖析

走进SkyWalking-插件概念[#2]

Skywalking系列博客3-Java Agent插件

Skywalking系列博客3-Java Agent插件

过时Skywalking Java 插件开发注意点

过时Skywalking Java 插件开发注意点