Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

目标

  • 无需在有无乱序数据时分开配置
  • 避免设置活跃的 partition 个数
  • 考虑 PrimitiveArrayPool 内存占用
  • 尽量有效利用内存,使 Chunk 尽量大
  • 尽量保证创建元数据成功,为保证内存不爆,可以拒绝写入
  • 尽量在有无乱序情况下,iotdb参数不需要变化都能很好适应
  • 尽量不阻塞写入
  • 尽量不加入固定参数

Image Added


所涉及的统计信息类

AbstractMemTable

包括以下两个内存统计值:

  • tvListRamCost:所有TVList被分配的内存总大小,包括TEXT值和primitive arrays中未被占用的空值

  • memSize:数据点实际占用的内存大小,包括TEXT值

两者的关系如图所示:

Image Added

从图中可以看出,memSize ≤ tvListRamCost

TsFileProcessorInfo

维护一个TsFileProcessor的内存占用,任何内存变动都需要向StorageGroupInfo汇报

  • memCost:所有ChunkMetadata占用的内存大小

StorageGroupInfo

维护一个存储组的内存占用,当内存占用的增量超过指定的阈值时向SystemInfo汇报

  • memoryCost:所有TsFileProcessor占用的ChunkMetadata、primitive arrays和TEXT值的内存总和,即∑ TsFileProcessorInfo.memCost + AbstractMemTable.tvListRamCost

SystemInfo

维护所有存储组的内存占用

  • totalStorageGroupMemCost:所有StorageGroupInfo中memroyCost的总和


写入流程各部分内存统计


RPC模块

  • 一次请求的大小受限制 thrift_max_frame_size=67108864

现有问题:

  1. 用户设置活跃的partition比较麻烦
  2. 有无乱序情况的最佳配置不一样
  3. PrimitiveArrayPool内存占用没考虑,容易爆内存
  4. 开启动态参数后创建时间序列经常失败
  5. 动态参数计算出的memtable偏小,chunk较小,影响查询性能
  6. 对象内存估计不准确
  7. 内存中一个时间序列点数过多,上1万,拷贝排序较慢

新策略:

目标(解决1-5)

尽量保证创建元数据成功,为保证内存不爆,可以拒绝写入

尽量在有无乱序情况下,iotdb参数不需要变化都能很好适应

让chunk大小最大化

尽量不阻塞写入

尽量不加入固定参数

Image Removed

RPC模块:

  • 一次请求的大小受限制 b.(防止许用户一条SQL写入1亿个点等场景;或者写了一个大于2GB的bytes[]).
  • 并发数受限制 c。

内存写入模块:

优点:

  1. 所有SG共享内存,不再对每个SG单独设置一个内存上限,因此创建序列(或今后改为序列活跃情况变化)时也不需要再更新SG;好处是内存利用率可以很高;

缺点:

  1. 部分步骤需要全局锁;目前看,假设array为k,SG info 写x延迟上报,则个memTable写入16MB后,会拿一次全局锁更新全局内存情况。
  • rpc_max_concurrent_client_num=65535。


核心思想:

  • Schema和历史resource单独分配大小;下文仅考虑其余写数据部分大小。
  • 每个SG统计自身的chunk_metadata和unseal_resource大小;
  • 全局ArrayPool统计buffered和out of buffer的array大小
  • 系统统计总的大小

数据写入流程

写入流程:


写入线程

  1. 如果是非空的写入线程
    • 在 StorageEngine 中检查SystemInfo是否为reject状态;如果是,则该写入线程循环sleep 50ms(等待flush线程释放内存,system置回正常状态)再进行写入;如果等待max_waiting_time_when_insert_blocked后仍为reject状态,抛出写入异常;
    • 进入对应的StorageGroupProcessor,获取
  2. 给定一个写入计划,找到其对应的StorageGroupProcessorTSP;
  3. 检查SystemInfo是否为reject状态;如果是,则该写入线程循环sleep 100ms 等待flush线程释放内存,system置回正常状态再进行写入;如果等待6000ms后仍为reject状态,抛出写入异常;
  4. 获取 StorageGroupProsessor 的
    • writeLock
    • 进入对应分区的 TsFileProcessor:(1)获取已有的可写入的顺序或乱序 TsFileProcessor(2)如果没有可写入的TsFileProcessor,创建新的 TsFileProcessor
      • 统计当前写入计划新增的内存占用,增加至TspInfo和SgInfo中:(1)新测点增加 chunk_
  5. metadata(2)新设备增加 unclosed resource(3)TEXT 类型数据(4)
      • metadata(2)TEXT 类型数据(3)TVList 中增加的
  6. PrimitiveArray(5)flush内存
      • PrimitiveArray(4)flush内存
      • 如果 SGInfo 增量超过阈值(storage
  7. 如果SGInfo变化超过System 上报的阈值(storage
      • _group_size_report_threshold=16M)
      • 向SystemInfo进行上报;
            • 向SystemInfo进行上报(将当前 TsFileProcessor 传入);
              synchronized(SystemInfo) {
          • 先更新
                  • 更新 SystemInfo
          • 内存占用 
                  • 内存占用。
                  • 如果 SystemInfo 内存占用 < 总写入内存 * flush_proportion,返回 true。
                  • 如果 总写入内存 * flush_
          • proportion <=
                  • proportion ≤ SystemInfo 内存占用 < 总写入内存 * reject_proportion,
          • 触发flush
                  • 执行 选择Memtable提交flush流程,返回 true。
                  • 如果 总写入内存 *
          • 80% <=
                  • reject_proportion ≤ SystemInfo 内存占用, SystemInfo 置为 reject
          • 状态,触发flush
          • 返回是否允许此次写入
            }
          • 如果返回禁止写入;如果是,则该写入线程循环sleep 100ms 等待flush线程释放内存,system置回正常状态再进行写入;如果等待30s后仍为reject状态,抛出写入异常;
          • 若此时捕获到写入异常,本次写入失败,返回客户端
          • 若写入正常,开始向working memtable中写入数据,当memtable中Array空间不足时,向Array Pool申请新的Array。array pool判断是否有已向系统报备过的该类型array(即Buffered array)
            1. 如果Buffered arrayArray Pool申请Buffered array并写入数据;
            2. 如果没有,则需要申请OOB(out of buffered)的数组

      Flush流程:

      flush 分为两种 一种为正常flush,为写入过程中触发,在insertPlan写入完成后正式开始进行异步flush(正常写入流程中flush);另一种为异步即时flush,即触发后立刻开始flush(flush释放内存后,若System总内存仍处于flush阈值之上且当前没有flush任务时触发)

      正常flush流程:

      1. 通过上报到system里的sgInfo,找到所有的TSP;
      2. 使用PriorityQueue pop出当前workMemTable内存占用最大的Top K个TSP,将其标记为shouldFlush;如果flush此memtable后系统仍在flush阈值以上,再从PriorityQueue pop出一个TSP做标记,直到这些memtable flush后会回到flush阈值一下或者PriorityQueue为空
      3. 写入完成后在StorageGroupProcessor里检查shouldFlush,如果为true,进行异步flush过程

      即时flush流程:

      1. 通过上报到system里的sgInfo,找到所有的TSP;
      2. 使用PriorityQueue pop出当前workMemTable内存占用最大的Top K个TSP,进行异步flush;如果flush此memtable后系统仍在flush阈值以上,再从PriorityQueue pop出一个TSP进行异步flush,直到这些memtable flush后会回到flush阈值一下或者PriorityQueue为空

      关闭TsFile文件逻辑:

      文件封口触发逻辑与现有master版本相同,都为一个insertPlan写入完成后,检查该TSP是否需要flush,如果需要,再检查是否TsFile大小超过阈值,如果超过,flush memtable后将文件封口。

              • 状态, 执行 选择Memtable提交flush流程,记返回值为 flag
                • 如果 flag = true 
                  • 如果 SystemInfo 内存占用 < 总写入内存,则返回 true
                  • 如果 SystemInfo 内存占用 ≥ 总写入内存,直接抛 写入Reject 异常
                • 如果 flag = false,则返回 false
                  }
            • 判断 向SystemInfo上报 的返回结果
              • 如果返回 false,则该写入线程循环 sleep (50ms) ,检查 SystemInfo 的 reject 状态如果不 reject或者该memtable被标记为shouldFlush,执行正常写入。如果等待 max_waiting_time_when_insert_blocked 后仍为reject状态,抛出写入异常
              • 如果返回 true,则执行正常写入
              • 如果捕获到 写入Reject 异常,reset SystemInfo,并继续向上抛
          • 检查 workingMemtable 的 shouldFlush,如果为true,提交 Flush 任务,并根据文件大小判断是否需要 close。
        • StorageGroupProsessor. 释放writeLock
      1. 如果是空的写入线程
        • 进入对应的 StorageGroupProcessor,获取 writeLock
        • 获取对应分区的 TsFileProcessor:如果(其 workingMemtable 不为空且 shouldFlush 为 true),则提交 flush 任务;否则直接返回。
        • StorageGroupProsessor. 释放writeLock


      选择Memtable提交flush流程

      1. 使用 PriorityQueue 对当前系统所有 memtable 按占用内存由大到小排序
      2. boolean flag = false
      3. 当前活跃内存 = SystemInfo 总内存 - SystemInfo flush内存
      4. 对 PriorityQueue 的每个 workingMemtable 逐个标记 shouldFlush,(直到标记的这些 TsFileProcessor 刷盘后 当前活跃内存 能降到 flush 阈值之下)
        1. 提交一个异步的空的写入线程(写入被标记的 Memtable 中)
        2. 判断此 workingMemtable 是否属于当前 TsFileProcessor,如果属于,flag = true
      5. 返回 flag


      Flush 线程:

      1. 先更新 SystemInfo flush 内存
      2. 将 workingMemtable 移到 flushingMemtables 中


      TsFile文件关闭逻辑:

      1. 一个insertPlan写入完成后,检查该TSP的 workingMemtable 的 shouldFlush 字段,如果为 true,再检查是否TsFile大小超过阈值,如果超过,flush memtable后将文件封口。
      2. TsFile关闭完成后,清空该TSPInfo,重置对应的 SGInfo 状态,并向SystemInfo报告重置后SGInfo

      ...

      1. 如果此时SystemInfo 为reject状态 且 `SystemInfo中统计的总内存 < 总写入内存 *
      2. 80%`,将SystemInfo
      3. reject_proportion`,将SystemInfo 置于正常状态
      4. 如果`SystemInfo中统计的总内存 >= 总写入内存 * 50%`,触发即时flush

       

      MTree内存控制:

      注册时间序列时,如果总时间序列个数*300 estimate_series_size > 总内存*0.1,此时拒绝注册,抛出异常。write_read_schema_free_memory_proportion:schema,此时拒绝注册,抛出异常。



      相关参数整理


      1. 是否开启内存控制
        enable_mem_control=true
        以下参数只在开启内存控制时生效: 
        1. flush阈值(0.0--1.0)(关闭内存控制后无效)
          当所有memtable实际占用大于总写入内存 * flush_proportion,触发flush
          flush_proportion=0.4
        2. reject阈值(0.0--1.0)(关闭内存控制后无效)
          当所有memtable实际占用大于总写入内存 * reject_proportion,阻塞写入,等待flush释放内存
          reject_proportion=0.8
        3. array pool内存占总写入内存的大小比例(0.0--1.0)
          buffered_arrays_memory_proportion=0.6
        4. sg上报阈值(bytes)(关闭内存控制后无效)
          当一个sg内所有memtable的内存相比上次上报的增量大于这个值,向SystemInfo更新目前Sg的总大小
          注意:当sg较多,例如1000时,需要考虑调小这个值。因为此时memtable需要写 16M * 1000 = 16G 才会向SystemInfo 汇报,比较危险。
          storage_group_report_threshold=16777216
        5. 阻塞写入后的检查周期(ms)(关闭内存控制后无效)
          当写入被SystemInfo拒绝后,客户端线程会以这个时间周期去检查SystemInfo的状态,直到flush线程释放掉一些内存,SystemInfo置回正常状态。
          check_period_when_insert_blocked=50
        6. 阻塞写入后的最大等待时间(ms)(关闭内存控制后无效)
          当写入阻塞时间超过这个值后,向客户端返回写入异常
          max_waiting_time_when_insert_blocked=10000
        7. 预估一条序列在mtree中的大小(关闭内存控制后无效)
          这个值用来限制可注册的序列个数
          estimated_series_size=300
      2. memtable大小阈值(bytes)(开启内存控制后无效)
        memtable_size_threshold=1073741824
      3. 写入、查询、schema、剩余内存占比
        其中可注册的序列的个数由schema的内存(byte)除以estimated_series_size来确定
        write_read_schema_free_memory_proportion=4:3:1:2
      4. array pool中的array长度
        平均每个chunk的点数小于这个值时会造成内存的浪费,可以考虑调小这个值
        primitive_array_size=128
      5. TsFile大小阈值
        0代表只刷一个memtable就关文件;1代表刷两个memtable才关文件;大于1时如1G时,1个文件中会存在更多的memtable
        目前设为1是考虑merge会把小文件合并为大文件;而且如果这个阈值过大且开启内存控制后,会导致内存中metadata积累较多,memtable越来越小
        tsfile_size_threshold=1
      6. 平均chunk点数阈值(个数)
        当memtable内平均每个序列的点数超过这个阈值时,触发flush
        avg_series_point_number_threshold=10000




      优点:

      1. 所有SG共享内存,不再对每个SG单独设置一个内存上限,因此创建序列(或今后改为序列活跃情况变化)时也不需要再更新SG;好处是内存利用率可以很高;

      缺点:

      1. 部分步骤需要全局锁;目前看,假设array为k,SG info 写x延迟上报,则个memTable写入16MB后,会拿一次全局锁更新全局内存情况。


      细节:关于Array Pool中分类型的数组如何管理?

      ...

      LRU缺点:有类型写入很快、有类型很慢,则抖动太厉害(慢速的加入池中,会踢出快类型的,然后还没被复用,就又被踢走了);LFU:负载变化时适应太慢;2Q。


      细节:为什么当SG的info变化超过一定阈值才向Sys汇报?细节:为什么当SG的info变化超过一定阈值才向SystemInfo汇报?

      解法:为了减少全局锁。假设Sys预留1GB出来,有n个SG,那么每个SG info每增长1/n GB数据时,才需要跟全局同步一次。GB数据时,才需要跟全局同步一次,减少contention。


      细节:String/byte[] 不需要buffer池的原因。

      ...

      在该方案中,只有图中红色部分是全局锁(ArrayPool, SysInfo,reject信号量)SystemInfo,reject信号量)


      详细计算公式:


        • WAL buffer: 一个存储组固定大小的buffer:b(永久)
        • PrimitiveArrayPool 原始类型数组缓存(永久)
          • memtable 写入数据使用
          • memtable 排序使用
          • String数组每次会清空(现状)。
        • TVListAllocator 中 TVList 对象头缓存(永久)
        • MemtablePool 中 Memtable 对象头缓存(永久)
        • flush 之后在内存中积累的 ChunkMetadata 缓存大小为 K(临时)
        • flush编码后的字节数组:一个Chunk编码后的大小(临时)

      ...

      系统级别维护SystemInfo:S

        • S=A+sum(G
      1. 若写入正常,开始向working memtable中写入数据,当memtable中Array空间不足时,向Array Pool申请新的Array。array pool判断是否有已向系统报备过的该类型array(即Buffered array)
        1. 如果Buffered arrayArray Pool申请Buffered array并写入数据;
        2. 如果没有,则需要申请OOB(out of buffered)的数组

      SG个数:M

      系统为delay上报预留的内存大小:R

      ...

      TsFile封口时,更新ChunkMetadata和Resource的内存统计并上报给System

      Historical Resource 设计

      采用二级索引的方式来降低TsFileResource常驻内存的数量,进而控制内存使用。

      rootFile: 记录是<device, [starttime, endTime, IndexFile]>,是TsFileResource的根索引,常驻内存

      IndexFile:记录是<device, [starttime, endTime, TsFileRecource]>, 是TsFileResource的中间索引,固定大小,按需load进内存

      TsFileResource:记录是<device, starttime, endTime>, 是TsFile的索引,按需load进内存

      写入

      TsFile关闭时,unsealed_resource刷入磁盘;维护IndexFile,将涉及的各device的tsfile resource都进行记录。

      如果IndexFile文件大于阈值(1GB)了,就进行关闭持久化到磁盘上,并开启一个新的IndexFile用于记录下一批TsFileResource中间索引。IndexFile关闭时,往rootFile里继续写一批记录,将涉及的各device的 index file都进行记录。

      细节:rootFile也过大怎么办?

      解法:多级索引(缺点,一次查询过多次访问磁盘);或者在rootFile中忽略一些device(缺点是这些设备的查询需要逐个去扫描indexFile)

      ...

      估算下一个rootFile可以索引多少个TsFileResource。

      假设:

      1个storage group,每个设备100个测点,每个storage group 51200个设备, 也就是5.12e6个测点

      内存128GB

      Tsfile 512MB

      IndexFile是1GB

      每条ResourceIndex 100B,一个IndexFile是1GB,可以记录1GB/100B=10240000条记录,也就是200个TsFileResource。

      每个rootFile在一个IndexFile关闭时会记录一下,也是51200*100B=5.12MB

       

      也就是200个TsFileResource会产1GB的IndexFile和5.12MB的rootFile

       

      100T的磁盘空间,有100T/512MB=100000个TsFile,需要100000/200 * 1GB = 500GB IndexFile,100000/200*5.12MB= 2.56GB内存

       

       

      假设一个storage group A个设备,一个设备D个测点, 总共就是A * D个测点

      假设每次TsFile刷写会造成所有设备的索引更新,就会产生A * 100B 的IndexFile记录

       

      一个IndexFile假设是 C GB, 那一个IndexFile能支持 C * 1000,000,000 / A * 100 = C * 10,000,000/A 个TsFile的刷写

       

      IndexFile关闭时,也会在RootFile里记录下A * 100B的rootFile记录

       

      也就是C * 10,000,000/A 个TsFile的刷写,会有C GB的IndexFile产生, 还有A * 100B的rootFile记录

       

      假设整个数据库有S个 Storage group,那么常驻内存的就是 S * (rootfile + C GB)

       

      A 平均一个storage group 设备数

      D 平均一个设备测点数

      C IndexFile文件大小,单位是GB

      512MB TsFile大小

       

      rootFile = C * 10,000,000/A * 100B

      IndexFile= C GB

      磁盘空间= C * 10,000,000/A * 512MB

      测点=A * D

       

       

      该估算有个问题就是对于TsFileName长度可以进行优化,可以只消耗Tsfile个数的TsFilename长度内存占用,而不是device * [starttime, endTime] * tsFile个数来估算,因此其内存占用估算放大了很多倍。

       

      这个优化的前提是java的string 常量池,可以保证多个string公用一个字符串常量。

       

      因此,限制一个StorageGroup的设备数是可以做到只使用(2.56GB+1GB)/128GB~ 3%的内存索引100TB的磁盘空间。

      读取流程

      1. 查找常驻内存的rootFile,找到对应的device的记录,根据startTime和endTime,找到对应的IndexFile。
      2. load IndexFile进内存,找到对应的device的记录,根据startTime和endTime,找到对应的TsFile。
      3. load TsFile在内存中构造TsFileResource

      merge流程

      TsFile会定期的跟乱序文件进行合并,因此其名字和元数据也会进行更新。

      对于新生成的TsFile,也需要往IndexFile里写入记录即<device, ResourceIndexs>, 但是这个会造成有多个indexFile里某条device的记录区间有重叠的情况。

      例如有IndexFile1记录的device, 10, 200, Tsfile1,  merge后,新的indexFile2记录device, 10,400, tsFile2。

      这两条记录都会在rootFile里进行记录,查询的时候需要读取两个IndexFile,但是IndexFile1里的TsFile1找不到了,那就不再查找。只找IndexFile2里的TsFile2.

       

      这里会多读一次文件,为了减少这种无效的索引干扰,可以在merge 数据结束后进行一下相关IndexFile的合并操作,尽量保证一个device的一个时间区间在一个IndexFile里。

      cache优化

      ...


      文档原链接: https://shimo.im/docs/CWxXTDhvkRrHvXPx