blob: d7c74dba3839c31f448dcb9d3e9c28cab57e7c1f [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#ifndef IMPALA_RUNTIME_BUFFER_POOL_H
#define IMPALA_RUNTIME_BUFFER_POOL_H
#include <stdint.h>
#include <string>
#include <vector>
#include <boost/scoped_ptr.hpp>
#include <boost/thread/locks.hpp>
#include "common/atomic.h"
#include "common/compiler-util.h"
#include "common/object-pool.h"
#include "common/status.h"
#include "gutil/macros.h"
#include "runtime/mem-tracker-types.h"
#include "runtime/tmp-file-mgr.h"
#include "util/aligned-new.h"
#include "util/internal-queue.h"
#include "util/mem-range.h"
#include "util/spinlock.h"
namespace impala {
class MetricGroup;
class ReservationTracker;
class RuntimeProfile;
class SystemAllocator;
/// A buffer pool that manages memory buffers for all queries in an Impala daemon.
/// The buffer pool enforces buffer reservations, limits, and implements policies
/// for moving spilled memory from in-memory buffers to disk. It also enables reuse of
/// buffers between queries, to avoid frequent allocations.
///
/// The buffer pool can be used for allocating any large buffers (above a configurable
/// minimum length), whether or not the buffers will be spilled. Smaller allocations
/// are not serviced directly by the buffer pool: clients of the buffer pool must
/// subdivide buffers if they wish to use smaller allocations.
///
/// All buffer pool operations are in the context of a registered buffer pool client.
/// A buffer pool client should be created for every allocator of buffers at the level
/// of granularity required for reporting and enforcement of reservations, e.g. an
/// operator. The client tracks buffer reservations via its ReservationTracker and also
/// includes info that is helpful for debugging (e.g. the operator that is associated
/// with the buffer). Unless otherwise noted, it is not safe to invoke concurrent buffer
/// pool operations for the same client.
///
/// Pages, Buffers and Pinning
/// ==========================
/// * A page is a logical block of memory that can reside in memory or on disk.
/// * A buffer is a physical block of memory that can hold a page in memory.
/// * A page handle is used by buffer pool clients to identify and access a page and
/// the corresponding buffer. Clients do not interact with pages directly.
/// * A buffer handle is used by buffer pool clients to identify and access a buffer.
/// * A page is pinned if it has pin count > 0. A pinned page stays mapped to the same
/// buffer.
/// * An unpinned page can be written out to disk by the buffer pool so that the buffer
/// can be used for another purpose.
///
/// Buffer/Page Sizes
/// =================
/// The buffer pool has a minimum buffer size, which must be a power-of-two. Page and
/// buffer sizes must be an exact power-of-two multiple of the minimum buffer size.
///
/// Reservations
/// ============
/// Before allocating buffers or pinning pages, a client must reserve memory through its
/// ReservationTracker. Reservation of n bytes give a client the right to allocate
/// buffers or pin pages summing up to n bytes. Reservations are both necessary and
/// sufficient for a client to allocate buffers or pin pages: the operations succeed
/// unless a "system error" such as a disk write error is encountered that prevents
/// unpinned pages from being written to disk.
///
/// More memory may be reserved than is used, e.g. if a client is not using its full
/// reservation. In such cases, the buffer pool can use the free buffers in any way,
/// e.g. for keeping unpinned pages in memory, so long as it is able to fulfill the
/// reservations when needed, e.g. by flushing unpinned pages to disk.
///
/// Page/Buffer Handles
/// ===================
/// The buffer pool exposes PageHandles and BufferHandles, which are owned by clients of
/// the buffer pool, and act as a proxy for the internal data structure representing the
/// page or buffer in the buffer pool. Handles are "open" if they are associated with a
/// page or buffer. An open PageHandle is obtained by creating a page. PageHandles are
/// closed by calling BufferPool::DestroyPage(). An open BufferHandle is obtained by
/// allocating a buffer or extracting a BufferHandle from a PageHandle. The buffer of a
/// pinned page can also be accessed through the PageHandle. The handle destructors check
/// for resource leaks, e.g. an open handle that would result in a buffer leak.
///
/// Pin Counting of Page Handles:
/// ----------------------------------
/// Page handles are scoped to a client. The invariants are as follows:
/// * A page can only be accessed through an open handle.
/// * A page is destroyed once the handle is destroyed via DestroyPage().
/// * A page's buffer can only be accessed through a pinned handle.
/// * Pin() can be called on an open handle, incrementing the handle's pin count.
/// * Unpin() can be called on a pinned handle, but not an unpinned handle.
/// * Pin() always increases usage of reservations, and Unpin() always decreases usage,
/// i.e. the handle consumes <pin count> * <page size> bytes of reservation.
///
/// Example Usage: Buffers
/// ==================================
/// The simplest use case is to allocate a memory buffer.
/// * The new buffer is created with AllocateBuffer().
/// * The client reads and writes to the buffer as it sees fit.
/// * If the client is done with the buffer's contents it can call FreeBuffer() to
/// destroy the handle and free the buffer, or use TransferBuffer() to transfer
/// the buffer to a different client.
///
/// Example Usage: Spillable Pages
/// ==============================
/// * In order to spill pages to disk, the Client must be registered with a FileGroup,
/// which is used to allocate scratch space on disk.
/// * A spilling operator creates a new page with CreatePage().
/// * The client reads and writes to the page's buffer as it sees fit.
/// * If the operator encounters memory pressure, it can decrease reservation usage by
/// calling Unpin() on the page. The page may then be written to disk and its buffer
/// repurposed internally by BufferPool.
/// * Once the operator needs the page's contents again and has sufficient unused
/// reservation, it can call Pin(), which brings the page's contents back into memory,
/// perhaps in a different buffer. Therefore the operator must fix up any pointers into
/// the previous buffer. Pin() executes asynchronously - the caller only blocks waiting
/// for read I/O if it calls GetBuffer() or ExtractBuffer() while the read is in
/// flight.
/// * If the operator is done with the page, it can call DestroyPage() to destroy the
/// handle and release resources, or call ExtractBuffer() to extract the buffer.
///
/// Synchronization
/// ===============
/// The data structures in the buffer pool itself are thread-safe. Client-owned data
/// structures - Client, PageHandle and BufferHandle - are not protected from concurrent
/// accesses. Clients must ensure that they do not invoke concurrent operations with the
/// same Client, PageHandle or BufferHandle.
class BufferPool : public CacheLineAligned {
public:
class BufferAllocator;
class BufferHandle;
class ClientHandle;
class PageHandle;
class SubReservation;
/// Constructs a new buffer pool.
/// 'min_buffer_len': the minimum buffer length for the pool. Must be a power of two.
/// 'buffer_bytes_limit': the maximum physical memory in bytes that can be used by the
/// buffer pool. If 'buffer_bytes_limit' is not a multiple of 'min_buffer_len', the
/// remainder will not be usable.
/// 'clean_page_bytes_limit': the maximum bytes of clean pages that will be retained by
/// the buffer pool.
BufferPool(MetricGroup* metrics, int64_t min_buffer_len, int64_t buffer_bytes_limit,
int64_t clean_page_bytes_limit);
~BufferPool();
/// Register a client. Returns an error status and does not register the client if the
/// arguments are invalid. 'name' is an arbitrary name used to identify the client in
/// any errors messages or logging. If 'file_group' is non-NULL, it is used to allocate
/// scratch space to write unpinned pages to disk. If it is NULL, unpinning of pages is
/// not allowed for this client. Counters for this client are added to the (non-NULL)
/// 'profile'. 'client' is the client to register. 'client' must not already be
/// registered.
///
/// The client's reservation is created as a child of 'parent_reservation' with limit
/// 'reservation_limit' and associated with MemTracker 'mem_tracker'. The initial
/// reservation is 0 bytes. 'mem_limit_mode' determines whether reservation
/// increases are checked against the soft or hard limit of 'mem_tracker'.
Status RegisterClient(const std::string& name, TmpFileMgr::FileGroup* file_group,
ReservationTracker* parent_reservation, MemTracker* mem_tracker,
int64_t reservation_limit, RuntimeProfile* profile, ClientHandle* client,
MemLimit mem_limit_mode = MemLimit::SOFT) WARN_UNUSED_RESULT;
/// Deregister 'client' if it is registered. All pages must be destroyed and buffers
/// must be freed for the client before calling this. Releases any reservation that
/// belongs to the client. Idempotent.
void DeregisterClient(ClientHandle* client);
/// Create a new page of 'len' bytes with pin count 1. 'len' must be a page length
/// supported by BufferPool (see BufferPool class comment). The client must have
/// sufficient unused reservation to pin the new page (otherwise it will DCHECK).
/// CreatePage() only fails when a system error prevents the buffer pool from fulfilling
/// the reservation.
/// On success, the handle is mapped to the new page and 'buffer', if non-NULL, is set
/// to the page's buffer.
Status CreatePage(ClientHandle* client, int64_t len, PageHandle* handle,
const BufferHandle** buffer = nullptr) WARN_UNUSED_RESULT;
/// Increment the pin count of 'handle'. After Pin() the underlying page will
/// be mapped to a buffer, which will be accessible through 'handle'. If the data
/// was evicted from memory, it will be read back into memory asynchronously.
/// Attempting to access the buffer with ExtractBuffer() or handle.GetBuffer() will
/// block until the data is in memory. The caller is responsible for ensuring it has
/// enough unused reservation before calling Pin() (otherwise it will DCHECK). Pin()
/// only fails when a system error prevents the buffer pool from fulfilling the
/// reservation or if an I/O error is encountered reading back data from disk.
/// 'handle' must be open.
Status Pin(ClientHandle* client, PageHandle* handle) WARN_UNUSED_RESULT;
/// Decrement the pin count of 'handle'. Decrease client's reservation usage. If the
/// handle's pin count becomes zero, it is no longer valid for the underlying page's
/// buffer to be accessed via 'handle'. If the page's total pin count across all
/// handles that reference it goes to zero, the page's data may be written to disk and
/// the buffer reclaimed. 'handle' must be open and have a pin count > 0.
///
/// It is an error to reduce the pin count to 0 if 'client' does not have an associated
/// FileGroup.
void Unpin(ClientHandle* client, PageHandle* handle);
/// Destroy the page referenced by 'handle' (if 'handle' is open). Any buffers or disk
/// storage backing the page are freed. Idempotent. If the page is pinned, the
/// reservation usage is decreased accordingly.
void DestroyPage(ClientHandle* client, PageHandle* handle);
/// Extracts buffer from a pinned page. After this returns, the page referenced by
/// 'page_handle' will be destroyed and 'buffer_handle' will reference the buffer from
/// 'page_handle'. This may decrease reservation usage of 'client' if the page was
/// pinned multiple times via 'page_handle'. May return an error if 'page_handle' was
/// unpinned earlier with no subsequent GetBuffer() call and a read error is
/// encountered while bringing the page back into memory.
Status ExtractBuffer(ClientHandle* client, PageHandle* page_handle,
BufferHandle* buffer_handle) WARN_UNUSED_RESULT;
/// Allocates a new buffer of 'len' bytes. Uses reservation from 'client'. The caller
/// is responsible for ensuring it has enough unused reservation before calling
/// AllocateBuffer() (otherwise it will DCHECK). AllocateBuffer() only fails when
/// a system error prevents the buffer pool from fulfilling the reservation.
/// Safe to call concurrently with any other operations for 'client', except for
/// operations on the same 'handle'.
Status AllocateBuffer(
ClientHandle* client, int64_t len, BufferHandle* handle) WARN_UNUSED_RESULT;
/// Like AllocateBuffer(), except used when the client may not have the reservation
/// to allocate the buffer. Tries to increase reservation on the behalf of the client
/// if needed to allocate the buffer. If the reservation isn't available, 'handle'
/// isn't opened and OK is returned. If an unexpected error occurs, an error is
/// returned and any reservation increase remains in effect. Safe to call concurrently
/// with any other operations for 'client', except for operations on the same 'handle'.
///
/// This function is a transitional mechanism for components to allocate memory from
/// the buffer pool without implementing the reservation accounting required to operate
/// within a predetermined memory constraint. Wherever possible, clients should reserve
/// memory ahead of time and allocate out of that instead of relying on this "best
/// effort" interface.
Status AllocateUnreservedBuffer(
ClientHandle* client, int64_t len, BufferHandle* handle) WARN_UNUSED_RESULT;
/// If 'handle' is open, close 'handle', free the buffer and decrease the reservation
/// usage from 'client'. Idempotent. Safe to call concurrently with other operations
/// for 'client', except for operations on the same 'handle'.
void FreeBuffer(ClientHandle* client, BufferHandle* handle);
/// Transfer ownership of buffer from 'src_client' to 'dst_client' and move the
/// handle from 'src' to 'dst'. Increases reservation usage in 'dst_client' and
/// decreases reservation usage in 'src_client'. 'src' must be open and 'dst' must be
/// closed before calling. 'src'/'dst' and 'src_client'/'dst_client' must be different.
/// After a successful call, 'src' is closed and 'dst' is open. Safe to call
/// concurrently with any other operations for 'src_client', except for operations
/// on the same handles.
Status TransferBuffer(ClientHandle* src_client, BufferHandle* src,
ClientHandle* dst_client, BufferHandle* dst) WARN_UNUSED_RESULT;
/// Try to release at least 'bytes_to_free' bytes of memory to the system allocator.
/// TODO: once IMPALA-4834 is done and all large allocations are served from the buffer
/// pool, this may not be necessary.
void ReleaseMemory(int64_t bytes_to_free);
/// Called periodically by a maintenance thread to release unused memory back to the
/// system allocator.
void Maintenance();
/// Print a debug string with the state of the buffer pool.
std::string DebugString();
int64_t min_buffer_len() const { return min_buffer_len_; }
int64_t GetSystemBytesLimit() const;
int64_t GetSystemBytesAllocated() const;
/// Return the limit on bytes of clean pages in the pool.
int64_t GetCleanPageBytesLimit() const;
/// Return the total number of clean pages in the pool.
int64_t GetNumCleanPages() const;
/// Return the total bytes of clean pages in the pool.
int64_t GetCleanPageBytes() const;
/// Return the total number of free buffers in the pool.
int64_t GetNumFreeBuffers() const;
/// Return the total bytes of free buffers in the pool.
int64_t GetFreeBufferBytes() const;
/// Generous upper bounds on page and buffer size and the number of different
/// power-of-two buffer sizes.
static constexpr int LOG_MAX_BUFFER_BYTES = 48;
static constexpr int64_t MAX_BUFFER_BYTES = 1L << LOG_MAX_BUFFER_BYTES;
protected:
friend class BufferPoolTest;
/// Test helper: get a reference to the allocator.
BufferAllocator* allocator() { return allocator_.get(); }
private:
DISALLOW_COPY_AND_ASSIGN(BufferPool);
class Client;
class FreeBufferArena;
class PageList;
struct Page;
/// Allocator for allocating and freeing all buffer memory and managing lists of free
/// buffers and clean pages.
boost::scoped_ptr<BufferAllocator> allocator_;
/// The minimum length of a buffer in bytes. All buffers and pages are a power-of-two
/// multiple of this length. This is always a power of two.
const int64_t min_buffer_len_;
};
/// External representation of a client of the BufferPool. Clients are used for
/// reservation accounting, and will be used in the future for tracking per-client
/// buffer pool counters. This class is the external handle for a client so
/// each Client instance is owned by the BufferPool's client, rather than the BufferPool.
/// Each Client should only be used by a single thread at a time: concurrently calling
/// Client methods or BufferPool methods with the Client as an argument is not supported.
class BufferPool::ClientHandle {
public:
ClientHandle() : impl_(NULL) {}
/// Client must be deregistered.
~ClientHandle() { DCHECK(!is_registered()); }
/// Request to increase reservation for this client by 'bytes' by calling
/// ReservationTracker::IncreaseReservation(). Returns true if the reservation was
/// successfully increased. Thread-safe.
bool IncreaseReservation(int64_t bytes) WARN_UNUSED_RESULT;
/// Tries to ensure that 'bytes' of unused reservation is available for this client
/// to use by calling ReservationTracker::IncreaseReservationToFit(). Returns true
/// if successful, after which 'bytes' can be used. Thread-safe.
bool IncreaseReservationToFit(int64_t bytes) WARN_UNUSED_RESULT;
/// Try to decrease this client's reservation down to a minimum of 'target_bytes' by
/// releasing up to 'max_decrease' bytes of unused reservation to ancestor
/// ReservationTrackers, all the way up to the root of the ReservationTracker tree.
/// May block waiting for unpinned pages to be flushed. This client's reservation
/// must be at least 'target_bytes' before calling this method. May fail if decreasing
/// the reservation requires flushing unpinned pages to disk and a write to disk fails.
/// Thread-safe.
Status DecreaseReservationTo(
int64_t max_decrease, int64_t target_bytes) WARN_UNUSED_RESULT;
/// Move some of this client's reservation to the SubReservation. 'bytes' of unused
/// reservation must be available in this tracker.
void SaveReservation(SubReservation* dst, int64_t bytes);
/// Move some of src's reservation to this client. 'bytes' of unused reservation must be
/// available in 'src'.
void RestoreReservation(SubReservation* src, int64_t bytes);
/// Accessors for this client's reservation corresponding to the identically-named
/// methods in ReservationTracker.
int64_t GetReservation() const;
int64_t GetUsedReservation() const;
int64_t GetUnusedReservation() const;
/// Try to transfer 'bytes' of reservation from 'src' to this client using
/// ReservationTracker::TransferReservationTo(). Not valid to call if 'this'
/// has unpinned pages.
bool TransferReservationFrom(ReservationTracker* src, int64_t bytes);
/// Transfer 'bytes' of reservation from this client to 'dst' using
/// ReservationTracker::TransferReservationTo().
bool TransferReservationTo(ReservationTracker* dst, int64_t bytes);
/// Call SetDebugDenyIncreaseReservation() on this client's ReservationTracker.
void SetDebugDenyIncreaseReservation(double probability);
bool is_registered() const { return impl_ != NULL; }
/// Return true if there are any unpinned pages for this client.
bool has_unpinned_pages() const;
std::string DebugString() const;
private:
friend class BufferPool;
friend class BufferPoolTest;
friend class SubReservation;
DISALLOW_COPY_AND_ASSIGN(ClientHandle);
/// Internal state for the client. NULL means the client isn't registered.
/// Owned by BufferPool.
Client* impl_;
};
/// Helper class that allows dividing up a client's reservation into separate buckets.
class BufferPool::SubReservation {
public:
// Construct without initializing this SubReservation.
SubReservation();
// Construct and initialize with 'client' as the parent.
SubReservation(ClientHandle* client);
~SubReservation();
// Initialize with 'client' as the parent.
void Init(ClientHandle* client);
/// Returns the amount of reservation stored in this sub-reservation.
int64_t GetReservation() const;
/// Releases the sub-reservation to the client's tracker. Must be called before
/// destruction if this was initialized.
void Close();
bool is_closed() const { return tracker_ == nullptr; }
private:
friend class BufferPool::ClientHandle;
DISALLOW_COPY_AND_ASSIGN(SubReservation);
/// Child of the client's tracker used to track the sub-reservation. Usage is not
/// tracked against this tracker - instead the reservation is always transferred back
/// to the client's tracker before use.
boost::scoped_ptr<ReservationTracker> tracker_;
};
/// A handle to a buffer allocated from the buffer pool. Each BufferHandle should only
/// be used by a single thread at a time: concurrently calling BufferHandle methods or
/// BufferPool methods with the BufferHandle as an argument is not supported.
class BufferPool::BufferHandle {
public:
BufferHandle() { Reset(); }
~BufferHandle() { DCHECK(!is_open()); }
/// Allow move construction of handles to support std::move(). Inline to make moving
/// efficient.
inline BufferHandle(BufferHandle&& src);
/// Allow move assignment of handles to support STL classes like std::vector.
/// Destination must be uninitialized. Inline to make moving efficient.
inline BufferHandle& operator=(BufferHandle&& src);
bool is_open() const { return data_ != NULL; }
int64_t len() const {
DCHECK(is_open());
return len_;
}
/// Get a pointer to the start of the buffer.
uint8_t* data() const {
DCHECK(is_open());
return data_;
}
MemRange mem_range() const { return MemRange(data(), len()); }
std::string DebugString() const;
/// Poison the memory associated with this handle. If ASAN is not enabled, this is a
/// no-op.
void Poison() { ASAN_POISON_MEMORY_REGION(data(), len()); }
/// Unpoison the memory associated with this handle. If ASAN is not enabled, this is a
/// no-op.
void Unpoison() { ASAN_UNPOISON_MEMORY_REGION(data(), len()); }
private:
DISALLOW_COPY_AND_ASSIGN(BufferHandle);
friend class BufferPool;
friend class SystemAllocator;
/// Internal helper to set the handle to an opened state.
void Open(uint8_t* data, int64_t len, int home_core);
/// Internal helper to reset the handle to an unopened state. Inlined to make moving
/// efficient.
inline void Reset();
/// The client the buffer handle belongs to, used to validate that the correct client
/// is provided in BufferPool method calls. Set to NULL if the buffer is in a free list.
const ClientHandle* client_;
/// Pointer to the start of the buffer. Non-NULL if open, NULL if closed.
uint8_t* data_;
/// Length of the buffer in bytes.
int64_t len_;
/// The CPU core that the buffer was allocated from - used to determine which arena
/// it will be added to.
int home_core_;
};
/// The handle for a page used by clients of the BufferPool. Each PageHandle should
/// only be used by a single thread at a time: concurrently calling PageHandle methods
/// or BufferPool methods with the PageHandle as an argument is not supported.
class BufferPool::PageHandle {
public:
PageHandle();
~PageHandle() { DCHECK(!is_open()); }
// Allow move construction of page handles, to support std::move().
PageHandle(PageHandle&& src);
// Allow move assignment of page handles, to support STL classes like std::vector.
// Destination must be closed.
PageHandle& operator=(PageHandle&& src);
bool is_open() const { return page_ != NULL; }
bool is_pinned() const { return pin_count() > 0; }
int pin_count() const;
int64_t len() const;
/// Get a reference to the page's buffer handle. Only valid to call if the page is
/// pinned. If the page was previously unpinned and the read I/O for the data is still
/// in flight, this can block waiting. Returns an error if an error was encountered
/// reading the data back, which can only happen if Unpin() was called on the page
/// since the last call to GetBuffer(). Only const accessors of the returned handle can
/// be used: it is invalid to call FreeBuffer() or TransferBuffer() on it or to
/// otherwise modify the handle.
Status GetBuffer(const BufferHandle** buffer_handle) const WARN_UNUSED_RESULT;
std::string DebugString() const;
private:
DISALLOW_COPY_AND_ASSIGN(PageHandle);
friend class BufferPool;
friend class BufferPoolTest;
friend struct Page;
/// Internal helper to open the handle for the given page.
void Open(Page* page, ClientHandle* client);
/// Internal helper to reset the handle to an unopened state.
void Reset();
/// The internal page structure. NULL if the handle is not open.
Page* page_;
/// The client the page handle belongs to.
ClientHandle* client_;
};
inline BufferPool::BufferHandle::BufferHandle(BufferHandle&& src) {
Reset();
*this = std::move(src);
}
inline BufferPool::BufferHandle& BufferPool::BufferHandle::operator=(
BufferHandle&& src) {
DCHECK(!is_open());
// Copy over all members then close src.
client_ = src.client_;
data_ = src.data_;
len_ = src.len_;
home_core_ = src.home_core_;
src.Reset();
return *this;
}
inline void BufferPool::BufferHandle::Reset() {
client_ = NULL;
data_ = NULL;
len_ = -1;
home_core_ = -1;
}
}
#endif