环境

IoTDB:fit4,fit5,fit7 (三节点三副本)

benchmark:fit4,fit5,fit7(每个bm实例连接本机的IoTDB)

IoTDB version: 31f077ed752b759442c7f95aa4f003dbdb229260

benchmark barnch: 0.12

benchmark config:

config.properties

IoTDB config:

conf.zip


现象

2021-05-24 11:52:22,226 服务端启动
2021-05-24 17:39:37,642 服务端发生connection reset
2021-05-25 04:44:47,377 服务端开始报OOM


分析

使用JProfile对fit4进行内存分析,发现分配给查询的内存约为5.5GB,而实际上ChunkCache占用了约8GB内存

使用MBean查看ChunkCache相关统计信息,确认ChunkCache的使用内存存在严重超额

原因排查

根据代码,ChunkCache如果满了之后,会立刻踢出10%的缓存,除非LRUMap中没有数据了

通过在代码中加入debug日志发现,上图的while循环退出时,usedMemory依旧大于retailMemory且iterator.hasNext()为false,但是打印Map.size()可以发现,map其实并不为空。

查看LinkedHashMap的源码可以发现,内部除了哈希表外,还维护了一个链表用以保存节点顺序,迭代器是通过链表对所有节点进行遍历的,所以怀疑是代码中有并发问题,导致链表与哈希表不一致了。

因为LRU需要记录最近最少使用的节点,所以在每一次get操作,都要调整链表,是对链表的写操作,而我们对get操作仅仅加了读锁,所以get操作能够并发写链表,导致链表结构被破坏。


问题解决

一个朴素的想法是get操作也加互斥锁,但是这样并发性就会很低,读读之间都会互相阻塞。

在网上找到了两个比较成熟的java的Cache的实现,并集成在我们IoTDB中

分支名:ChunkCacheBug,使用Google Guava Cache(https://github.com/google/guava/wiki/CachesExplained)

分支名:ChunkCacheCaffeine,使用Caffeine Cache(https://github.com/ben-manes/caffeine)

Caffeine是基于Guava Cache实现的,在其基础上又做了很多工程上的优化,在官方性能测试上,碾压了Guava Cache(https://github.com/ben-manes/caffeine/wiki/Benchmarks)

用我们的周测场景去测试这两个分支与之前的错误实现的master分支(读读直接并发,读写、写写阻塞),发现这两种实现对查询性能几乎没有影响,出于Caffeine官方对比性能测试的考虑,采用Caffeine实现。


  • No labels