需求

当一条查询sql涉及的列数过多,如: select * from root;服务器端内存无法容纳这么多列的查询结果集,就会抛出OOM异常。这条异常查询还会影响到其他正常查询。

解决方案(旧)

总体思想

限制每条查询能够涉及的总列数上限(由配置文件中的max_deduplicated_path_num参数决定,默认值为1000),单次查询实际能够涉及的列数与指定的fetch_size有关,fetch_size越大,则列数越小。

  1. 所有查询结果集总内存:allocateMemoryForReadWithoutCache。默认占查询总分配内存的3/10,由配置参数chunkmeta_chunk_timeseriesmeta_free_memory_proportion决定
  2. 单个点占内存16B,这个版本不考虑text类型的点。
  3. 在QueryResourceManager中维护查询剩余可用总内存,以及每个查询预估占用内存。
  4. 对于一个新查询的到来,根据公式Math.min((int) ((totalFreeMemoryForRead.get() / fetchSize) / POINT_ESTIMATED_SIZE),MAX_COLUMN_SUM)计算得到该查询最大的列数。
  5. 构建查询计划时
    1. 若发现实际列数超过步骤4得到的最大列数,则抛出PathNumOverLimitException异常,查询返回。
    2. 若发现实际列数低于步骤4得到的最大列数,在向QueryResourceManager申请queryID的同时,将fetch_size和actual_column_num作为参数传递,QueryResourceManager中计算该查询预计需要占用的内存,并更新查询剩余可用总内存。
      1. 若更新后小于0,则查询失败;
      2. 若更新成功,则在内部记录该queryID对应查询的占用内存
  6. 查询结束时,向QueryResourceManager申请关闭查询资源时,再同时归还查询剩余可用总内存。


基本框架(新)

之前设计的那版,是根据查询的序列数估算内存占用大小,即内存大小 = pathNum(列数) * fetchSize(行数)* 单位内存大小

存在几个问题:

  1. 数据在读取过程中产生的内存占用均没有被计算在内,而只估算了缓存结果集的大小

  2. 即使只返回 fetchSize 行数据,但是由于 chunk 大小不确定,且有乱序文件的存在,导致最后得到的 batchData 的大小可能大于 fetchSize 的大小,或者说至少大于等于该值。

  3. 无法计算对于 string 类型的结果进行准确的估算。


因此,新版查询内存控制的框架如下:

定义一个固定大小的内存池,同时考虑数据读取过程中产生的内存占用以及结果集中缓存结果的大小

对于内存超限的场景,

  • 对于未提交的查询任务,设计两个检查点,一个是元数据去 * 的时候,另一个是提交 QueryTask 任务的时候,如果检查到查询内存超限,则暂时不获取元数据或不提交查询任务

  • 对于已经提交的查询任务,则...


需要考虑的问题:

  1. 数据读取过程中需要考虑哪些内存占用?

    • 结论:读出的 chunk 以及 batchData

  2. 对于内存超限场景的处理:

    • 超限的标准是什么,假如有一部分空间,怎么判断这部分空间是否足够本查询执行?

      • 结论:以最优情况判断,即只查询也只包含了结果集大小的数据,如果对于 String 类型的数据,则以固定字节计算。

    • 查询内存超限时,对于未提交的查询任务,应该怎么处理?

      • 结论:Sleep() 等待,如果等待时间过长超时则返回错误

    • 查询内存超限时,对于已经在执行的查询任务,应该怎么处理?

      • 结论:

  3. 对于并行查询,是平均分配内存,还是共享总的查询内存?

    • 结论:共享总的内存池



1. 数据读取

数据读取过程中产生的内存占用:

  1. 元数据占用内存

    • 物理计划、时间序列路径

  2. SeriesReader 占用内存

    • TimeSeriesMetaData、chunkMetaData

      • 最优情况:无乱序数据的情况下,则最多缓存一个TimeSeriesMetaData,之后会被转化为List<ChunkMetaData>

      • 最坏情况:乱序数据较多的情况下,最多缓存重叠数个TimeSeriesMetaData,之后这些TimeSeriesMetaData会被同时转化为ChunkMetaData。

      • 以上计算时不考虑 Cache

    • Chunk

      • 和 MetaData 一样,会同时解开当前所有重叠的chunk,表现形式为List<PageReader>

    • BatchData

      • 无乱序数据的情况下,以 Page 大小为单位,如果有 filter,则小于等于 Page 大小

      • 有乱序数据的情况下,单个 batchData 的大小取决于重叠的时间区间内数据点的个数


实际结果导向,非预估

2. 结果集缓存

  1. 原始数据查询

    • 不带值过滤

      • 直接生成的字节流结果集

    • 带值过滤

      • 每次获取 fetchSize 大小的时间戳,根据时间戳查询 RowRecord, 最大 cache fetchsize 大小的 rowRecord
        List<RowRecord> cachedRowRecords
        每次 Next 调用会 Remove 头部的 rowRecord
  2. 聚合查询

    • 序列数个AggregateResult[]

  3. Group by 查询

    • 每个时间区间缓存序列数个AggregateResult[]

    • 每次只缓存一个时间区间,挨个计算

  4. Last 查询

    • 每个序列缓存一行结果

  5. Align by device 查询

    • 建立在各个查询的基础上




查询内存占用分析

测试环境:macOS 2G堆内存,默认配置

写入50亿数据点,单存储组,5个设备,10个测点,无乱序

原始数据查询(不带值过滤)

执行 SQL:select ** from root

首次查询(无缓存),执行过程中,多数为 blockingQueue 中缓存的 batchData,缓存数量由序列数和blockingQueue大小共同决定。


查询执行结束后,常驻内存则以 ChunkCache 为主:


原始数据查询(带值过滤)

执行 SQL:select * from root.test.*.* where s_1 = true

内存占用中,一半为读取的 BatchData,另一半为缓存的 RowRecord,RowRecord 大小由 fetchSize 决定



聚合查询(不带值过滤)

执行 SQL:select count(*) from root.test.*.*

由于均为顺序数据,拿 statistics 就解决了,产生的内存占用极少。


聚合查询(带值过滤)

执行 SQL:select count(*) from root.test.*.* where s_1 = true

和带值过滤的原始数据查询相似,一半为查询得到的 batchData,另一部分为从 BatchData 中计算出的结果,表现形式为 Object[]



Group by 查询(不带值过滤)

执行 SQL:

select count(*) from root.test.*.* group by ([2018-09-20T00:08:25.000+08:00, 2034-07-25T01:01:40.000+08:00), 1d)

由于不带值过滤的 Group by 查询,要么选择从统计信息中更新,要么直接从 BatchData 中更新,所以内存占用几乎均为 BatchData。




Group by 查询(带值过滤)

执行 SQL:

select count(*) from root.test.*.* where s_1 = true group by ([2018-09-20T00:08:25.000+08:00, 2034-07-25T01:01:40.000+08:00), 1d)

内存占用情况与不带值过滤的 Group by 查询相似,因为计算过程相同,只是读取 BatchData 的过程不同。




  • No labels