Versions Compared

Key

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

...

于是我们开始分析查询对堆外内存的使用,最直接的想到我们的 TsFile 在读取时使用了 FileChannel 类,FileChannel 在读取时会申请临时的堆外内存,但是我们当时对于 FileChannel 申请堆外内存的原理,包括 buffer 大小和生命周期都不熟悉,因此只能看 JVM 源码,找资料,做实验。

有正确的方向总是好事,通过努力我们发现: 源码,找资料,做实验。有正确的方向总是好事,通过努力我们发现: FileChannel 会将读取时申请的堆外内存存在一个 buffer 池里,这个 buffer 池的大小是 1024(根据机器可能有所不同),每个 buffer 的大小由一个参数控制,而每个 buffer 池又是 ThreadLocal 的,只要线程仍然存活,这个 buffer 池内的堆外内存缓存就不会释放。也就是说,我们给查询开了 CPU 核数的线程池,这个线程池的查询线程会一直存活,且每个线程读取 tsfile 文件时都会开一个 fileChannel,因此堆外内存的总占用为:

查询线程数 *  同时打开的文件数 * buffer 池大小(1024) * 单个 buffer大小 * 同时打开的文件数。buffer大小。


以上为 JVM 源码

TEMP_BUF_POOL_SIZE: 临时堆外内存 buffer 池的大小

MAX_CACHED_BUFFER_SIZE:每个 buffer 的最大值,在 JDK8u102 之后添加,之前甚至对单个 buffer 的大小上限没有任何限制。

bufferCache: 临时堆外内存池




这个时候问题原因已经大致清晰了。天远的机器是 这个时候问题原因已经大致清晰了。天远的机器是 80核,也就是我们整整开了 80 个查询子线程,而且天远的 tsFile 文件大小非常大,最大的文件达到了 30G,单个 chunk 大小为 500M,也就是说即使我们不考虑 buffer 池子有大小这件事,仅仅缓存一个 chunk 大小的 buffer,也耗了 80 * 500M = 40G 的堆外内存!




因此,结合资料和我们自己的推断,一方面由于查询线程池贯穿整个 iotdb 进程的生命周期,且每个查询线程在读取文件时都会直接申请堆外内存,因此必须对查询线程的数量进行限制。另一方面,虽然 JDK 8u102 之后的版本对 buffer 的最大 size 进行了限制,但是 JDK 的发行说明也提到,这个参数只能解决部分问题,因为该申请的依然会申请,只不过大于阈值的 buffer 会在使用后释放,小于阈值的会被缓存下来。因此在遇到临时申请 buffer 过大,或者小 buffer 缓存数量多的情况,依然会对堆外内存产生较大的压力,因此要从根本上对合并产生的 chunk 大小进行限制,避免因单个 chunk 过大而临时申请堆外超出限制的情况。过大而临时申请堆外内存超出限制的情况。