facebook / CacheLib

Pluggable in-process caching engine to build and scale high performance services

Home Page:https://www.cachelib.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How can I understand better why some time allocation fail

Chokoabigail opened this issue · comments

This is my cache implementation:

int32_t PutKey(CacheKey key, const std::string& value)
{
	size_t valueSize = value.size();
	size_t numChunks = GetChunkNUmber(valueSize);

	// parenthesize will hold all the pointers to the chained items
	size_t parentItemSize = numChunks * sizeof(void*);
	if (parentItemSize > gMaxChainedSize)
	{
		// For now - this is too big, later we will implement a chain of chains (currently we support > 2GB)
		if (gDbg)
		{
			cout << "[-][PutKey] Error parentItemSize:" << parentItemSize << " is bigger then gMaxChainedSize:" << gMaxChainedSize << endl;
		}

		return 2;
	}

	// Get parrent
	auto parentItemHandle = gCache_->allocate(defaultPool_, key, parentItemSize);
	if (!parentItemHandle)
	{
		if (gDbg)
		{
			cout << "[-][PutKey] Error we didn't succeed to get the parentItemHandle" << endl;
		}

		return 3;
	}

	//Fill in the chunk number in the parent
	// CustomParentItem* parentItem = reinterpret_cast<CustomParentItem*>(parentItemHandle->getMemory());
	// parent item->numChunks = numChunks;

	// Get char * representation of the data
	char* ourValue = const_cast<char*>(value.c_str());

	// Create the chunk - Now split user data into chunks and cache them
	for (size_t i = 0; i < numChunks; ++i) {
		size_t chunkSize = std::min((size_t)(gMaxChainedSize), valueSize);
		auto chainedItemHandle = gCache_->allocateChainedItem(parentItemHandle, chunkSize);
		if (!chainedItemHandle)
		{
			//We failed to allocate the chunk
			if (gDbg)
			{
				cout << "[-][PutKey] Error we didn't succeed to allocateChainedItem for size:" << chunkSize << endl;
			}
			return 4;
		}

		// Compute user data offset and copy data over
		char * dataOffset = ourValue + (gMaxChainedSize * i);
		std::memcpy(chainedItemHandle->getMemory(), dataOffset, chunkSize);

		// Add this chained item to the parent item
		gCache_->addChainedItem(parentItemHandle, std::move(chainedItemHandle));
		valueSize -= chunkSize;
	}

	// Now, make the parent item visible to others
	gCache_->insertOrReplace(parentItemHandle);

	return 1; // 1 == Success
}

When I run this, I get from time to time 4 or 3 in response (i.e., we failed in allocate/allocateChainedItem).
A. How should I handle that? should I retry allocate/allocateChainedItem before returning the error code?
B. How can I debug the root cause of this?

p.s.
My cache is an hybrid cache (with NVME), in the tests I assign to the RAM part 100mb.
This is my full settings:


// Set Nvme Cache 	
	nvmConfig.navyConfig.setBlockSize(4096); // Default is 4096 - Device block size in bytes (minimum IO granularity)
	nvmConfig.navyConfig.setSimpleFile(gCacheFilePath,
								858993459200, 		/*fileSize*/
								false 			/*truncateFile*/);

	// Set the block cache - Large Object Cache - caches objects that are larger than KBs in size 
	nvmConfig.navyConfig.blockCache().setRegionSize(33554432);

	// Set admission policy
	nvmConfig.navyConfig.enableRandomAdmPolicy().setAdmProbability(0.9); // a float number that decides Acceptance probability. The value has to be in the range of [0, 1].
	nvmConfig.enableFastNegativeLookups = true;

	// Set the Small Object Cache - caches objects that are 100s of bytes
	nvmConfig.navyConfig.bigHash()
      .setSizePctAndMaxItemSize(5, 4052) // bigHashSizePct (0-100), the % of the file that bigHash will use , bigHashSmallItemMaxSize must be smaller than bucket size which is 4096 by default
      .setBucketSize(4096)
      .setBucketBfSize(8); // bloom filter size per bucket, default is 8
	  

	// Ram config

	config
		.setCacheSize(1073741824) // 1GB = 1 * 1024 * 1024 * 1024
		.setCacheName("My Case")
		.setAccessConfig(40000000) // assuming caching 20 = {25 /* bucket power */, 10 /* lock power */})
		.enableNvmCache(nvmConfig)
			.validate(); // will throw if bad config


Hi @Chokoabigail,

allocate can fail if CacheAllocator cannot find items to be evicted at the end of eviction queue. The maximum number of retries (say N) is determined by evictionSearchTries and the eviction will be failed if none of the N items at the end of eviction queue for given allocation class are eligible to be evicted; i.e.,

To debug this, you can check how many handles are outstanding by checking CacheAllocator::getNumActiveHandles.

If the number is much higher than expected, you need to take a look at the client code for any leaking handles.

Otherwise if the number is expected, I think you can retry after some delay.

Thanks.

The client code is actually very simple, the most complex function is the PutKey and in it, there is a concern for memory leaks.

What should I do with this code:

auto chainedItemHandle = gCache_->allocateChainedItem(parentItemHandle, chunkSize);
if (!chainedItemHandle)
{
...
}

It potentially can be after several successful chainedItemHandle allocations (or none), should I call gCache_->remove(key); on the same key I used in auto parentItemHandle = gCache_->allocate(defaultPool_, key, parentItemSize);?
Should I call free on parentItemHandle ? Should I call free on every successful chainedItemHandle allocation before the loop?

This is the rest of the client code:


std::string GetCacheStatus()
{
	std::string res;
	std::string seperator = "[::]";

	res += "isNvmCacheEnabled: " + std::to_string(gCache_->isNvmCacheEnabled()) + seperator;

	// Get the statistics map
	auto statsMap = gCache_->getNvmCacheStatsMap();
	for (const auto& kv : statsMap.getCounts()) {
		std::string k = "statsMap.getCounts()" + kv.first + ":";
		res += k + std::to_string(gCache_->isNvmCacheEnabled()) + seperator;
	}

	// Get the rate map
	for (const auto& kv : statsMap.getRates()) {
		std::string k = "statsMap.getRates()" + kv.first + ":";
		res += k + std::to_string(gCache_->isNvmCacheEnabled()) + seperator;
	}

	return res;
}



bool RemoveKey(CacheKey key)
{
	auto rr = gCache_->remove(key);
	if (rr == Cache::RemoveRes::kSuccess) {
		return true;
	}
	return false;
}

std::string GetKey(CacheKey key) 
{
	std::string result;
	uint32_t fregmentNumber = 0;

	auto parent = gCache_->find(key);
	if (!parent)
	{
		return result;
	}

	auto iobuf_chainedItems = gCache_->convertToIOBuf(std::move(parent));
			
	for (const auto& item : iobuf_chainedItems) 
	{
		// First item is the parent item so we can continue over it
		if (fregmentNumber == 0)
		{
			fregmentNumber++;
			continue;
		}

		// Reconstruct the value
		folly::StringPiece sp2{item};
		result += sp2;
	}

	return result; 
}

@Chokoabigail As long as the handle is disposed, you are good. Those have zero references, so should be eligible to be evicted.

I can see you allocated 100MB for RAM cache, meaning you would have only 24 slabs or so. Some allocation class might not have enough slabs allocated or even 0. I would suggest check the distribution of slabs to alloc classes and what was the failing allocation class.

@Chokoabigail As long as the handle is disposed, you are good. Those have zero references, so should be eligible to be evicted.

I can see you allocated 100MB for RAM cache, meaning you would have only 24 slabs or so. Some allocation class might not have enough slabs allocated or even 0. I would suggest check the distribution of slabs to alloc classes and what was the failing allocation class.

How can I do that? (p.s. In production, we have the same client with a 16GB of RAM, and it also fails there from time to time (once a month or so))

@Chokoabigail You can refer to the ACStats which is part of the PoolStats returned by CacheAllocator::getPoolStats(...)

I noticed that there is no public support to print those stats. I think you can refer to the cachebench implementation here (-report_ac_memory_usage_stat option)

Let me close this for now. Feel free to reopen or open a new one if needed