Mooncake/mooncake-store/include/allocator.h

274 lines
8.7 KiB
C++

#ifndef BUFFER_ALLOCATOR_H
#define BUFFER_ALLOCATOR_H
#include <atomic>
#include <limits>
#include <memory>
#include <string>
#include "cachelib_memory_allocator/MemoryAllocator.h"
#include "offset_allocator/offset_allocator.hpp"
#include "types.h"
using facebook::cachelib::MemoryAllocator;
using facebook::cachelib::PoolId;
namespace mooncake {
/**
* @brief Type of buffer allocator used in the system
*/
enum class ReplicaType {
MEMORY, // Memory replica
DISK, // Disk replica
LOCAL_DISK, // Local disk replica
NOF_SSD, // Nvme-oF SSD replica
ALL, // All memory and NoF replicas in put finalize path
};
// Constant for unknown free space in allocators that don't track it precisely
static constexpr size_t kAllocatorUnknownFreeSpace =
std::numeric_limits<size_t>::max();
// Forward declarations
class BufferAllocatorBase;
class AllocatedBuffer {
public:
friend class CachelibBufferAllocator;
friend class OffsetBufferAllocator;
// Forward declaration of the descriptor struct
struct Descriptor;
AllocatedBuffer(std::shared_ptr<BufferAllocatorBase> allocator,
void* buffer_ptr, std::size_t size,
std::optional<offset_allocator::OffsetAllocationHandle>&&
offset_handle = std::nullopt)
: allocator_(std::move(allocator)),
buffer_ptr_(buffer_ptr),
size_(size),
offset_handle_(std::move(offset_handle)) {}
~AllocatedBuffer();
AllocatedBuffer(const AllocatedBuffer&) = delete;
AllocatedBuffer& operator=(const AllocatedBuffer&) = delete;
AllocatedBuffer(AllocatedBuffer&&) noexcept;
AllocatedBuffer& operator=(AllocatedBuffer&&) noexcept;
[[nodiscard]] void* data() const noexcept { return buffer_ptr_; }
[[nodiscard]] std::size_t size() const noexcept { return this->size_; }
[[nodiscard]] bool isAllocatorValid() const {
return !allocator_.expired();
}
// Serialize the buffer into a descriptor for transfer
[[nodiscard]] Descriptor get_descriptor() const;
[[nodiscard]] std::string getSegmentName() const noexcept;
// Friend declaration for operator<<
friend std::ostream& operator<<(std::ostream& os,
const AllocatedBuffer& buffer);
// Represents the serializable state
struct Descriptor {
uint64_t size_;
uintptr_t buffer_address_;
std::string protocol_;
std::string transport_endpoint_;
YLT_REFL(Descriptor, size_, buffer_address_, protocol_,
transport_endpoint_);
};
void change_to_cxl(std::string client_segment_name);
void* get_vaddr_from_cxl();
private:
std::weak_ptr<BufferAllocatorBase> allocator_;
std::string segment_name_;
void* buffer_ptr_{nullptr};
std::size_t size_{0};
std::string protocol{"tcp"};
// RAII handle for buffer allocated by offset allocator
std::optional<offset_allocator::OffsetAllocationHandle> offset_handle_{
std::nullopt};
friend class Serializer<AllocatedBuffer>;
};
/**
* Virtual base class for buffer allocators.
* Defines the interface that all buffer allocators must implement.
*/
class BufferAllocatorBase {
public:
virtual ~BufferAllocatorBase() = default;
virtual std::unique_ptr<AllocatedBuffer> allocate(size_t size) = 0;
virtual void deallocate(AllocatedBuffer* handle) = 0;
virtual size_t capacity() const = 0;
virtual size_t size() const = 0;
virtual std::string getSegmentName() const = 0;
virtual std::string getTransportEndpoint() const = 0;
/**
* Returns the largest free region available in this allocator.
* For CacheLib allocators, this returns kAllocatorUnknownFreeSpace as an
* approximation. For OffsetAllocator, this returns the actual largest free
* region.
*
* Note: This is a best-effort estimate used for filtering. The actual
* allocation may still fail due to race conditions or fragmentation.
*/
virtual size_t getLargestFreeRegion() const = 0;
};
/**
* CachelibBufferAllocator manages memory allocation using CacheLib's slab
* allocation strategy.
*
* Important alignment requirements:
* 1. Base address must be at least 8-byte aligned (CacheLib requirement)
* 2. Base address should be 4MB aligned since the total size must be a multiple
* of 4MB
* 3. Use sufficiently high base addresses (e.g., 0x100000000 for 4GB) to avoid
* memory conflicts
*
* Example usage:
* ```cpp
* // Good - properly aligned addresses
* const size_t base = 0x100000000; // 4GB aligned
* const size_t base = 0x200000000; // 8GB aligned
*
* // Bad - will likely crash
* const size_t base = 0x1234; // Too low, unaligned
* const size_t base = 0x100000001; // Not 4MB aligned
* ```
*/
class CachelibBufferAllocator
: public BufferAllocatorBase,
public std::enable_shared_from_this<CachelibBufferAllocator> {
public:
CachelibBufferAllocator(std::string segment_name, size_t base, size_t size,
std::string transport_endpoint,
ReplicaType replica_type = ReplicaType::MEMORY);
~CachelibBufferAllocator() override;
std::unique_ptr<AllocatedBuffer> allocate(size_t size) override;
void deallocate(AllocatedBuffer* handle) override;
size_t capacity() const override { return total_size_; }
size_t size() const override { return cur_size_.load(); }
std::string getSegmentName() const override { return segment_name_; }
std::string getTransportEndpoint() const override {
return transport_endpoint_;
}
/**
* For CacheLib, return kAllocatorUnknownFreeSpace as we don't have exact
* free region info. This ensures CacheLib allocators are always considered
* for allocation.
*/
size_t getLargestFreeRegion() const override {
return kAllocatorUnknownFreeSpace;
}
private:
// metadata
const std::string segment_name_;
const size_t base_;
const size_t total_size_;
std::atomic_size_t cur_size_;
const std::string transport_endpoint_;
const ReplicaType replica_type_;
// metrics - removed allocated_bytes_ member
// ylt::metric::gauge_t* allocated_bytes_{nullptr};
// cachelib
std::unique_ptr<char[]> header_region_start_;
size_t header_region_size_;
std::unique_ptr<facebook::cachelib::MemoryAllocator> memory_allocator_;
facebook::cachelib::PoolId pool_id_;
};
/**
* OffsetBufferAllocator manages memory allocation using the OffsetAllocator
* strategy, which provides efficient memory allocation with bin-based
* optimization.
*/
class OffsetBufferAllocator
: public BufferAllocatorBase,
public std::enable_shared_from_this<OffsetBufferAllocator> {
public:
OffsetBufferAllocator(std::string segment_name, size_t base, size_t size,
std::string transport_endpoint,
ReplicaType replica_type = ReplicaType::MEMORY);
~OffsetBufferAllocator() override;
std::unique_ptr<AllocatedBuffer> allocate(size_t size) override;
void deallocate(AllocatedBuffer* handle) override;
size_t capacity() const override { return total_size_; }
size_t size() const override { return cur_size_.load(); }
std::string getSegmentName() const override { return segment_name_; }
std::string getTransportEndpoint() const override {
return transport_endpoint_;
}
/**
* Returns the actual largest free region from the offset allocator.
*/
size_t getLargestFreeRegion() const override;
// Public method to get offset_allocator
std::shared_ptr<offset_allocator::OffsetAllocator> getOffsetAllocator()
const {
return offset_allocator_;
}
private:
// metadata
const std::string segment_name_;
const size_t base_;
const size_t total_size_;
std::atomic_size_t cur_size_;
const std::string transport_endpoint_;
const ReplicaType replica_type_;
// offset allocator implementation
std::shared_ptr<offset_allocator::OffsetAllocator> offset_allocator_;
friend class Serializer<OffsetBufferAllocator>;
};
// The main difference is that it allocates real memory and returns it, while
// BufferAllocator allocates an address
class SimpleAllocator {
public:
SimpleAllocator(size_t size);
~SimpleAllocator();
void* allocate(size_t size);
void deallocate(void* ptr, size_t size);
void* getBase() const { return base_; }
private:
void* base_{nullptr};
std::unique_ptr<char[]> header_region_start_;
size_t header_region_size_;
std::unique_ptr<facebook::cachelib::MemoryAllocator> memory_allocator_;
facebook::cachelib::PoolId pool_id_;
};
} // namespace mooncake
#endif // BUFFER_ALLOCATOR_H