Bug: OOM Error (Direct buffer memory) during intensive reading from large SSTables
Status: Triage Needed
Priority: Normal
Created: 01/Oct/20 07:25

Hello,

We have a certain database, from when we are reading intensively leads to the following OOM error:
{noformat}
 java.lang.OutOfMemoryError: Direct buffer memory
 at java.nio.Bits.reserveMemory(Bits.java:694) ~[na:1.8.0_212]
 at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) ~[na:1.8.0_212]
 at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) ~[na:1.8.0_212]
 at org.apache.cassandra.utils.memory.BufferPool.allocate(BufferPool.java:110) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.memory.BufferPool.access$1000(BufferPool.java:46) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.memory.BufferPool$LocalPool.allocate(BufferPool.java:407) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.memory.BufferPool$LocalPool.access$000(BufferPool.java:334) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.memory.BufferPool.takeFromPool(BufferPool.java:122) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.memory.BufferPool.get(BufferPool.java:94) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.cache.ChunkCache.load(ChunkCache.java:155) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.cache.ChunkCache.load(ChunkCache.java:39) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at com.github.benmanes.caffeine.cache.BoundedLocalCache$BoundedLocalLoadingCache.lambda$new$0(BoundedLocalCache.java:2949) ~[caffeine-2.2.6.jar:na]
 at com.github.benmanes.caffeine.cache.BoundedLocalCache.lambda$doComputeIfAbsent$15(BoundedLocalCache.java:1807) ~[caffeine-2.2.6.jar:na]
 at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1853) ~[na:1.8.0_212]
 at com.github.benmanes.caffeine.cache.BoundedLocalCache.doComputeIfAbsent(BoundedLocalCache.java:1805) ~[caffeine-2.2.6.jar:na]
 at com.github.benmanes.caffeine.cache.BoundedLocalCache.computeIfAbsent(BoundedLocalCache.java:1788) ~[caffeine-2.2.6.jar:na]
 at com.github.benmanes.caffeine.cache.LocalCache.computeIfAbsent(LocalCache.java:97) ~[caffeine-2.2.6.jar:na]
 at com.github.benmanes.caffeine.cache.LocalLoadingCache.get(LocalLoadingCache.java:66) ~[caffeine-2.2.6.jar:na]
 at org.apache.cassandra.cache.ChunkCache$CachingRebufferer.rebuffer(ChunkCache.java:235) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.cache.ChunkCache$CachingRebufferer.rebuffer(ChunkCache.java:213) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.util.RandomAccessReader.reBufferAt(RandomAccessReader.java:65) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.util.RandomAccessReader.seek(RandomAccessReader.java:207) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.util.FileHandle.createReader(FileHandle.java:150) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.sstable.format.SSTableReader.getFileDataInput(SSTableReader.java:1767) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.columniterator.AbstractSSTableIterator.<init>(AbstractSSTableIterator.java:103) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.columniterator.SSTableIterator.<init>(SSTableIterator.java:49) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.sstable.format.big.BigTableReader.iterator(BigTableReader.java:72) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.io.sstable.format.big.BigTableReader.iterator(BigTableReader.java:65) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIteratorWithLowerBound.initializeIterator(UnfilteredRowIteratorWithLowerBound.java:107) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.LazilyInitializedUnfilteredRowIterator.maybeInit(LazilyInitializedUnfilteredRowIterator.java:48) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIteratorWithLowerBound.getPartitionIndexLowerBound(UnfilteredRowIteratorWithLowerBound.java:191) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIteratorWithLowerBound.lowerBound(UnfilteredRowIteratorWithLowerBound.java:88) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIteratorWithLowerBound.lowerBound(UnfilteredRowIteratorWithLowerBound.java:47) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.MergeIterator$Candidate.<init>(MergeIterator.java:362) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.MergeIterator$ManyToOne.<init>(MergeIterator.java:147) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.utils.MergeIterator.get(MergeIterator.java:44) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIterators$UnfilteredRowMergeIterator.<init>(UnfilteredRowIterators.java:381) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIterators$UnfilteredRowMergeIterator.create(UnfilteredRowIterators.java:397) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIterators$UnfilteredRowMergeIterator.access$000(UnfilteredRowIterators.java:360) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.rows.UnfilteredRowIterators.merge(UnfilteredRowIterators.java:121) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.SinglePartitionReadCommand.withSSTablesIterated(SinglePartitionReadCommand.java:693) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.SinglePartitionReadCommand.queryMemtableAndDiskInternal(SinglePartitionReadCommand.java:639) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.SinglePartitionReadCommand.queryMemtableAndDisk(SinglePartitionReadCommand.java:514) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.SinglePartitionReadCommand.queryStorage(SinglePartitionReadCommand.java:376) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.db.ReadCommand.executeLocally(ReadCommand.java:407) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy$LocalReadRunnable.runMayThrow(StorageProxy.java:1882) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy$DroppableRunnable.run(StorageProxy.java:2587) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_212]
 at org.apache.cassandra.concurrent.AbstractLocalAwareExecutorService$FutureTask.run(AbstractLocalAwareExecutorService.java:162) ~[apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.concurrent.SEPExecutor.maybeExecuteImmediately(SEPExecutor.java:194) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.AbstractReadExecutor.makeRequests(AbstractReadExecutor.java:117) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.AbstractReadExecutor.makeDataRequests(AbstractReadExecutor.java:85) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.AbstractReadExecutor$NeverSpeculatingReadExecutor.executeAsync(AbstractReadExecutor.java:220) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy$SinglePartitionReadLifecycle.doInitialQueries(StorageProxy.java:1786) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy.fetchRows(StorageProxy.java:1739) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy.readRegular(StorageProxy.java:1682) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.service.StorageProxy.read(StorageProxy.java:1597) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.CassandraServer.read(CassandraServer.java:96) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.CassandraServer.getSlice(CassandraServer.java:263) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.CassandraServer.multigetSliceInternal(CassandraServer.java:570) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.CassandraServer.getSliceInternal(CassandraServer.java:323) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.CassandraServer.get_slice(CassandraServer.java:300) [apache-cassandra-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.Cassandra$Processor$get_slice.getResult(Cassandra.java:3659) [apache-cassandra-thrift-3.11.0.jar:3.11.0]
 at org.apache.cassandra.thrift.Cassandra$Processor$get_slice.getResult(Cassandra.java:3643) [apache-cassandra-thrift-3.11.0.jar:3.11.0]
 at org.apache.thrift.ProcessFunction.process(ProcessFunction.java:39) [libthrift-0.9.2.jar:0.9.2]
 at org.apache.thrift.TBaseProcessor.process(TBaseProcessor.java:39) [libthrift-0.9.2.jar:0.9.2]
 at org.apache.cassandra.thrift.CustomTThreadPoolServer$WorkerProcess.run(CustomTThreadPoolServer.java:206) [apache-cassandra-3.11.0.jar:3.11.0]
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_212]
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_212]
 at org.apache.cassandra.concurrent.NamedThreadFactory.lambda$threadLocalDeallocator$0(NamedThreadFactory.java:81) [apache-cassandra-3.11.0.jar:3.11.0]
 at java.lang.Thread.run(Thread.java:748) ~[na:1.8.0_212]{noformat}
This happens when we are doing large numbers of small reads from a CF that has many Gigabytes worth of data and it has SSTables that are up to almost 7 GB. I have tried to investigate/debug the issue myself, however, I have no definitive answer to myself yet whether this an expected result in given conditions or is it a bug.

What I have found so far is that the if I stop the whole VM on OutOfMemoryError, or just before it happens, I can see that ChunkCache is exceeding its maximum size (which is by default 512MiB in my case) and it is exceeding it by a lot. For example, I find that in a situation of a usual load, the cache would have near 8K entries maximum. This seems logical, since roughly that many chunks of 64kb are needed to fill the cache. However, when it runs out of memory it would find the cache containing more than 50K entries. This is more than 10 times of its specified maximum size. My question is: is that expected in a heavy load situation and what measures should I take to handle this?

If this is not expected, then I see it being potentially caused by the Caffeine cache that is used for ChunkCache. After looking into its implementation and any information that I could find on the internet, I am pretty confident that it is quite expected that this cache may exceed its maximum size.

I would have yet to write a stress test for the cache that would be intensively loading into it by multiple threads (which happens in Cassandra a lot), but I am pretty sure that it is possible to provoke this cache to go into a "runaway" situation where eviction from it wouldn't simply keep up at times. Besides, I could not find any mechanism within that cache that would allow for a substantial throttling of new inserts/loads into it, or somehow forcing eviction after the maximum size is exceeded. To be exact, the eviction would happen, but it does not prevent other threads to continue inserting new entries into the cache, and also there seems to be by design (or bug) that these other threads may only "eventually" see the update of the state of the cache from "evicting" to "not evicting anymore", and thus not initiating subsequent evictions even though they're needed.

So my another question is the following: was it considered that Caffeine cache may exceed the specified maximum size when it was chosen for the ChunkCache, and thus is what I am observing a normal behavior in some heavy load situations?