blob: 41d3226c86b6bacba178a69320ee7da065caa0c9 [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_DISK_IO_MGR_H
#define IMPALA_RUNTIME_DISK_IO_MGR_H
#include <functional>
#include <list>
#include <vector>
#include <boost/scoped_ptr.hpp>
#include <boost/unordered_set.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition_variable.hpp>
#include "common/atomic.h"
#include "common/hdfs.h"
#include "common/object-pool.h"
#include "common/status.h"
#include "runtime/thread-resource-mgr.h"
#include "util/bit-util.h"
#include "util/error-util.h"
#include "util/internal-queue.h"
#include "util/lru-cache.h"
#include "util/runtime-profile.h"
#include "util/thread.h"
namespace impala {
class MemTracker;
/// Manager object that schedules IO for all queries on all disks and remote filesystems
/// (such as S3). Each query maps to one or more DiskIoRequestContext objects, each of which
/// has its own queue of scan ranges and/or write ranges.
//
/// The API splits up requesting scan/write ranges (non-blocking) and reading the data
/// (blocking). The DiskIoMgr has worker threads that will read from and write to
/// disk/hdfs/remote-filesystems, allowing interleaving of IO and CPU. This allows us to
/// keep all disks and all cores as busy as possible.
//
/// All public APIs are thread-safe. It is not valid to call any of the APIs after
/// UnregisterContext() returns.
//
/// For Readers:
/// We can model this problem as a multiple producer (threads for each disk), multiple
/// consumer (scan ranges) problem. There are multiple queues that need to be
/// synchronized. Conceptually, there are two queues:
/// 1. The per disk queue: this contains a queue of readers that need reads.
/// 2. The per scan range ready-buffer queue: this contains buffers that have been
/// read and are ready for the caller.
/// The disk queue contains a queue of readers and is scheduled in a round robin fashion.
/// Readers map to scan nodes. The reader then contains a queue of scan ranges. The caller
/// asks the IoMgr for the next range to process. The IoMgr then selects the best range
/// to read based on disk activity and begins reading and queuing buffers for that range.
/// TODO: We should map readers to queries. A reader is the unit of scheduling and queries
/// that have multiple scan nodes shouldn't have more 'turns'.
//
/// For Writers:
/// Data is written via AddWriteRange(). This is non-blocking and adds a WriteRange to a
/// per-disk queue. After the write is complete, a callback in WriteRange is invoked.
/// No memory is allocated within IoMgr for writes and no copies are made. It is the
/// responsibility of the client to ensure that the data to be written is valid and that
/// the file to be written to exists until the callback is invoked.
//
/// The IoMgr provides three key APIs.
/// 1. AddScanRanges: this is non-blocking and tells the IoMgr all the ranges that
/// will eventually need to be read.
/// 2. GetNextRange: returns to the caller the next scan range it should process.
/// This is based on disk load. This also begins reading the data in this scan
/// range. This is blocking.
/// 3. ScanRange::GetNext: returns the next buffer for this range. This is blocking.
//
/// The disk threads do not synchronize with each other. The readers and writers don't
/// synchronize with each other. There is a lock and condition variable for each request
/// context queue and each disk queue.
/// IMPORTANT: whenever both locks are needed, the lock order is to grab the context lock
/// before the disk lock.
//
/// Scheduling: If there are multiple request contexts with work for a single disk, the
/// request contexts are scheduled in round-robin order. Multiple disk threads can
/// operate on the same request context. Exactly one request range is processed by a
/// disk thread at a time. If there are multiple scan ranges scheduled via
/// GetNextRange() for a single context, these are processed in round-robin order.
/// If there are multiple scan and write ranges for a disk, a read is always followed
/// by a write, and a write is followed by a read, i.e. reads and writes alternate.
/// If multiple write ranges are enqueued for a single disk, they will be processed
/// by the disk threads in order, but may complete in any order. No guarantees are made
/// on ordering of writes across disks.
//
/// Resource Management: effective resource management in the IoMgr is key to good
/// performance. The IoMgr helps coordinate two resources: CPU and disk. For CPU,
/// spinning up too many threads causes thrashing.
/// Memory usage in the IoMgr comes from queued read buffers. If we queue the minimum
/// (i.e. 1), then the disks are idle while we are processing the buffer. If we don't
/// limit the queue, then it possible we end up queueing the entire data set (i.e. CPU
/// is slower than disks) and run out of memory.
/// For both CPU and memory, we want to model the machine as having a fixed amount of
/// resources. If a single query is running, it should saturate either CPU or Disk
/// as well as using as little memory as possible. With multiple queries, each query
/// should get less CPU. In that case each query will need fewer queued buffers and
/// therefore have less memory usage.
//
/// The IoMgr defers CPU management to the caller. The IoMgr provides a GetNextRange
/// API which will return the next scan range the caller should process. The caller
/// can call this from the desired number of reading threads. Once a scan range
/// has been returned via GetNextRange, the IoMgr will start to buffer reads for
/// that range and it is expected the caller will pull those buffers promptly. For
/// example, if the caller would like to have 1 scanner thread, the read loop
/// would look like:
/// while (more_ranges)
/// range = GetNextRange()
/// while (!range.eosr)
/// buffer = range.GetNext()
/// To have multiple reading threads, the caller would simply spin up the threads
/// and each would process the loops above.
//
/// To control the number of IO buffers, each scan range has a soft max capacity for
/// the number of queued buffers. If the number of buffers is at capacity, the IoMgr
/// will no longer read for that scan range until the caller has processed a buffer.
/// This capacity does not need to be fixed, and the caller can dynamically adjust
/// it if necessary.
//
/// As an example: If we allowed 5 buffers per range on a 24 core, 72 thread
/// (we default to allowing 3x threads) machine, we should see at most
/// 72 * 5 * 8MB = 2.8GB in io buffers memory usage. This should remain roughly constant
/// regardless of how many concurrent readers are running.
//
/// Buffer Management:
/// Buffers for reads are either a) allocated by the IoMgr and transferred to the caller,
/// b) cached HDFS buffers if the scan range uses HDFS caching, or c) provided by the
/// caller when constructing the scan range.
///
/// As a caller reads from a scan range, these buffers are wrapped in BufferDescriptors
/// and returned to the caller. The caller must always call Return() on the buffer
/// descriptor when it when it is done to allow recycling of the buffer descriptor and
/// the associated buffer (if there is an IoMgr-allocated or HDFS cached buffer).
///
/// Caching support:
/// Scan ranges contain metadata on whether or not it is cached on the DN. In that
/// case, we use the HDFS APIs to read the cached data without doing any copies. For these
/// ranges, the reads happen on the caller thread (as opposed to the disk threads).
/// It is possible for the cached read APIs to fail, in which case the ranges are then
/// queued on the disk threads and behave identically to the case where the range
/// is not cached.
/// Resources for these ranges are also not accounted against the reader because none
/// are consumed.
/// While a cached block is being processed, the block is mlocked. We want to minimize
/// the time the mlock is held.
/// - HDFS will time us out if we hold onto the mlock for too long
/// - Holding the lock prevents uncaching this file due to a caching policy change.
/// Therefore, we only issue the cached read when the caller is ready to process the
/// range (GetNextRange()) instead of when the ranges are issued. This guarantees that
/// there will be a CPU available to process the buffer and any throttling we do with
/// the number of scanner threads properly controls the amount of files we mlock.
/// With cached scan ranges, we cannot close the scan range until the cached buffer
/// is returned (HDFS does not allow this). We therefore need to defer the close until
/// the cached buffer is returned (BufferDescriptor::Return()).
//
/// Remote filesystem support (e.g. S3):
/// Remote filesystems are modeled as "remote disks". That is, there is a seperate disk
/// queue for each supported remote filesystem type. In order to maximize throughput,
/// multiple connections are opened in parallel by having multiple threads running per
/// queue. Also note that reading from a remote filesystem service can be more CPU
/// intensive than local disk/hdfs because of non-direct I/O and SSL processing, and can
/// be CPU bottlenecked especially if not enough I/O threads for these queues are
/// started.
//
/// TODO: IoMgr should be able to request additional scan ranges from the coordinator
/// to help deal with stragglers.
/// TODO: look into using a lock free queue
/// TODO: simplify the common path (less locking, memory allocations).
/// TODO: Break this up the .h/.cc into multiple files under an /io subdirectory.
//
/// Structure of the Implementation:
/// - All client APIs are defined in this file
/// - Internal classes are defined in disk-io-mgr-internal.h
/// - ScanRange APIs are implemented in disk-io-mgr-scan-range.cc
/// This contains the ready buffer queue logic
/// - DiskIoRequestContext APIs are implemented in disk-io-mgr-reader-context.cc
/// This contains the logic for picking scan ranges for a reader.
/// - Disk Thread and general APIs are implemented in disk-io-mgr.cc.
class DiskIoRequestContext;
class DiskIoMgr {
public:
class ScanRange;
/// This class is a small wrapper around the hdfsFile handle and the file system
/// instance which is needed to close the file handle in case of eviction. It
/// additionally encapsulates the last modified time of the associated file when it was
/// last opened.
class HdfsCachedFileHandle {
public:
/// Constructor will open the file
HdfsCachedFileHandle(const hdfsFS& fs, const char* fname, int64_t mtime);
/// Destructor will close the file handle
~HdfsCachedFileHandle();
hdfsFile file() const { return hdfs_file_; }
int64_t mtime() const { return mtime_; }
/// This method is called to release acquired resources by the cached handle when it
/// is evicted.
static void Release(HdfsCachedFileHandle** h);
bool ok() const { return hdfs_file_ != NULL; }
private:
hdfsFS fs_;
hdfsFile hdfs_file_;
int64_t mtime_;
};
/// Buffer struct that is used by the caller and IoMgr to pass read buffers.
/// It is is expected that only one thread has ownership of this object at a
/// time.
class BufferDescriptor {
public:
ScanRange* scan_range() { return scan_range_; }
uint8_t* buffer() { return buffer_; }
int64_t buffer_len() { return buffer_len_; }
int64_t len() { return len_; }
bool eosr() { return eosr_; }
/// Returns the offset within the scan range that this buffer starts at
int64_t scan_range_offset() const { return scan_range_offset_; }
/// Transfer ownership of buffer memory from 'mem_tracker_' to 'dst' and set
/// 'mem_tracker_' to 'dst'. 'mem_tracker_' and 'dst' must be non-NULL. Does not
/// check memory limits on 'dst': the caller should check the memory limit if a
/// different memory limit may apply to 'dst'. If the buffer was a client-provided
/// buffer, transferring is not allowed.
/// TODO: IMPALA-3209: revisit this as part of scanner memory usage revamp.
void TransferOwnership(MemTracker* dst);
/// Returns the buffer to the IoMgr. This must be called for every buffer
/// returned by GetNext()/Read() that did not return an error. This is non-blocking.
/// After calling this, the buffer descriptor is invalid and cannot be accessed.
void Return();
private:
friend class DiskIoMgr;
friend class DiskIoRequestContext;
BufferDescriptor(DiskIoMgr* io_mgr);
/// Return true if this is a cached buffer owned by HDFS.
bool is_cached() const {
return scan_range_->external_buffer_tag_
== ScanRange::ExternalBufferTag::CACHED_BUFFER;
}
/// Return true if this is a buffer owner by the client that was provided when
/// constructing the scan range.
bool is_client_buffer() const {
return scan_range_->external_buffer_tag_
== ScanRange::ExternalBufferTag::CLIENT_BUFFER;
}
/// Reset the buffer descriptor to an uninitialized state.
void Reset();
/// Resets the buffer descriptor state for a new reader, range and data buffer.
/// The buffer memory should already be accounted against MemTracker
void Reset(DiskIoRequestContext* reader, ScanRange* range, uint8_t* buffer,
int64_t buffer_len, MemTracker* mem_tracker);
DiskIoMgr* const io_mgr_;
/// Reader that this buffer is for.
DiskIoRequestContext* reader_;
/// The current tracker this buffer is associated with. After initialisation,
/// NULL for cached buffers and non-NULL for all other buffers.
MemTracker* mem_tracker_;
/// Scan range that this buffer is for. Non-NULL when initialised.
ScanRange* scan_range_;
/// buffer with the read contents
uint8_t* buffer_;
/// length of buffer_. For buffers from cached reads, the length is 0.
int64_t buffer_len_;
/// length of read contents
int64_t len_;
/// true if the current scan range is complete
bool eosr_;
/// Status of the read to this buffer. if status is not ok, 'buffer' is NULL
Status status_;
int64_t scan_range_offset_;
};
/// The request type, read or write associated with a request range.
struct RequestType {
enum type {
READ,
WRITE,
};
};
/// Represents a contiguous sequence of bytes in a single file.
/// This is the common base class for read and write IO requests - ScanRange and
/// WriteRange. Each disk thread processes exactly one RequestRange at a time.
class RequestRange : public InternalQueue<RequestRange>::Node {
public:
hdfsFS fs() const { return fs_; }
const char* file() const { return file_.c_str(); }
int64_t offset() const { return offset_; }
int64_t len() const { return len_; }
int disk_id() const { return disk_id_; }
RequestType::type request_type() const { return request_type_; }
protected:
RequestRange(RequestType::type request_type)
: fs_(NULL), offset_(-1), len_(-1), disk_id_(-1), request_type_(request_type) {}
/// Hadoop filesystem that contains file_, or set to NULL for local filesystem.
hdfsFS fs_;
/// Path to file being read or written.
std::string file_;
/// Offset within file_ being read or written.
int64_t offset_;
/// Length of data read or written.
int64_t len_;
/// Id of disk containing byte range.
int disk_id_;
/// The type of IO request, READ or WRITE.
RequestType::type request_type_;
};
/// Param struct for different combinations of buffering.
struct BufferOpts {
public:
// Set options for a read into an IoMgr-allocated or HDFS-cached buffer. Caching is
// enabled if 'try_cache' is true, the file is in the HDFS cache and 'mtime' matches
// the modified time of the cached file in the HDFS cache.
BufferOpts(bool try_cache, int64_t mtime)
: try_cache_(try_cache),
mtime_(mtime),
client_buffer_(NULL),
client_buffer_len_(-1) {}
/// Set options for an uncached read into an IoMgr-allocated buffer.
static BufferOpts Uncached() {
return BufferOpts(false, NEVER_CACHE, NULL, -1);
}
/// Set options to read the entire scan range into 'client_buffer'. The length of the
/// buffer, 'client_buffer_len', must fit the entire scan range. HDFS caching is not
/// enabled in this case.
static BufferOpts ReadInto(uint8_t* client_buffer, int64_t client_buffer_len) {
return BufferOpts(false, NEVER_CACHE, client_buffer, client_buffer_len);
}
private:
friend class ScanRange;
BufferOpts(
bool try_cache, int64_t mtime, uint8_t* client_buffer, int64_t client_buffer_len)
: try_cache_(try_cache),
mtime_(mtime),
client_buffer_(client_buffer),
client_buffer_len_(client_buffer_len) {}
/// If 'mtime_' is set to NEVER_CACHE, the file handle will never be cached, because
/// the modification time won't match.
const static int64_t NEVER_CACHE = -1;
/// If true, read from HDFS cache if possible.
const bool try_cache_;
/// Last modified time of the file associated with the scan range. If set to
/// NEVER_CACHE, caching is disabled.
const int64_t mtime_;
/// A destination buffer provided by the client, NULL and -1 if no buffer.
uint8_t* const client_buffer_;
const int64_t client_buffer_len_;
};
/// ScanRange description. The caller must call Reset() to initialize the fields
/// before calling AddScanRanges(). The private fields are used internally by
/// the IoMgr.
class ScanRange : public RequestRange {
public:
/// The initial queue capacity for this. Specify -1 to use IoMgr default.
ScanRange(int initial_capacity = -1);
virtual ~ScanRange();
/// Resets this scan range object with the scan range description. The scan range
/// is for bytes [offset, offset + len) in 'file' on 'fs' (which is NULL for the
/// local filesystem). The scan range must fall within the file bounds (offset >= 0
/// and offset + len <= file_length). 'disk_id' is the disk queue to add the range
/// to. If 'expected_local' is true, a warning is generated if the read did not
/// come from a local disk. 'buffer_opts' specifies buffer management options -
/// see the DiskIoMgr class comment and the BufferOpts comments for details.
/// 'meta_data' is an arbitrary client-provided pointer for any auxiliary data.
void Reset(hdfsFS fs, const char* file, int64_t len, int64_t offset, int disk_id,
bool expected_local, const BufferOpts& buffer_opts, void* meta_data = NULL);
void* meta_data() const { return meta_data_; }
bool try_cache() const { return try_cache_; }
bool expected_local() const { return expected_local_; }
int ready_buffers_capacity() const { return ready_buffers_capacity_; }
/// Returns the next buffer for this scan range. buffer is an output parameter.
/// This function blocks until a buffer is ready or an error occurred. If this is
/// called when all buffers have been returned, *buffer is set to NULL and Status::OK
/// is returned.
/// Only one thread can be in GetNext() at any time.
Status GetNext(BufferDescriptor** buffer) WARN_UNUSED_RESULT;
/// Cancel this scan range. This cleans up all queued buffers and
/// wakes up any threads blocked on GetNext().
/// Status is the reason the range was cancelled. Must not be ok().
/// Status is returned to the user in GetNext().
void Cancel(const Status& status);
/// return a descriptive string for debug.
std::string DebugString() const;
int64_t mtime() const { return mtime_; }
private:
friend class DiskIoMgr;
friend class DiskIoRequestContext;
/// Initialize internal fields
void InitInternal(DiskIoMgr* io_mgr, DiskIoRequestContext* reader);
/// Enqueues a buffer for this range. This does not block.
/// Returns true if this scan range has hit the queue capacity, false otherwise.
/// The caller passes ownership of buffer to the scan range and it is not
/// valid to access buffer after this call.
bool EnqueueBuffer(BufferDescriptor* buffer);
/// Cleanup any queued buffers (i.e. due to cancellation). This cannot
/// be called with any locks taken.
void CleanupQueuedBuffers();
/// Validates the internal state of this range. lock_ must be taken
/// before calling this.
bool Validate();
/// Maximum length in bytes for hdfsRead() calls.
int64_t MaxReadChunkSize() const;
/// Opens the file for this range. This function only modifies state in this range.
Status Open();
/// Closes the file for this range. This function only modifies state in this range.
void Close();
/// Reads from this range into 'buffer', which has length 'buffer_len' bytes. Returns
/// the number of bytes read. The read position in this scan range is updated.
Status Read(uint8_t* buffer, int64_t buffer_len, int64_t* bytes_read,
bool* eosr) WARN_UNUSED_RESULT;
/// Reads from the DN cache. On success, sets cached_buffer_ to the DN buffer
/// and *read_succeeded to true.
/// If the data is not cached, returns ok() and *read_succeeded is set to false.
/// Returns a non-ok status if it ran into a non-continuable error.
Status ReadFromCache(bool* read_succeeded) WARN_UNUSED_RESULT;
/// Pointer to caller specified metadata. This is untouched by the io manager
/// and the caller can put whatever auxiliary data in here.
void* meta_data_;
/// If true, this scan range is expected to be cached. Note that this might be wrong
/// since the block could have been uncached. In that case, the cached path
/// will fail and we'll just put the scan range on the normal read path.
bool try_cache_;
/// If true, we expect this scan range to be a local read. Note that if this is false,
/// it does not necessarily mean we expect the read to be remote, and that we never
/// create scan ranges where some of the range is expected to be remote and some of it
/// local.
/// TODO: we can do more with this
bool expected_local_;
DiskIoMgr* io_mgr_;
/// Reader/owner of the scan range
DiskIoRequestContext* reader_;
/// File handle either to hdfs or local fs (FILE*)
///
/// TODO: The pointer to HdfsCachedFileHandle is manually managed and should be
/// replaced by unique_ptr in C++11
union {
FILE* local_file_;
HdfsCachedFileHandle* hdfs_file_;
};
/// Tagged union that holds a buffer for the cases when there is a buffer allocated
/// externally from DiskIoMgr that is associated with the scan range.
enum class ExternalBufferTag { CLIENT_BUFFER, CACHED_BUFFER, NO_BUFFER };
ExternalBufferTag external_buffer_tag_;
union {
/// Valid if the 'external_buffer_tag_' is CLIENT_BUFFER.
struct {
/// Client-provided buffer to read the whole scan range into.
uint8_t* data;
/// Length of the client-provided buffer.
int64_t len;
} client_buffer_;
/// Valid and non-NULL if the external_buffer_tag_ is CACHED_BUFFER, which means
/// that a cached read succeeded and all the bytes for the range are in this buffer.
struct hadoopRzBuffer* cached_buffer_;
};
/// Lock protecting fields below.
/// This lock should not be taken during Open/Read/Close.
boost::mutex lock_;
/// Number of bytes read so far for this scan range
int bytes_read_;
/// Status for this range. This is non-ok if is_cancelled_ is true.
/// Note: an individual range can fail without the DiskIoRequestContext being
/// cancelled. This allows us to skip individual ranges.
Status status_;
/// If true, the last buffer for this scan range has been queued.
bool eosr_queued_;
/// If true, the last buffer for this scan range has been returned.
bool eosr_returned_;
/// If true, this scan range has been removed from the reader's in_flight_ranges
/// queue because the ready_buffers_ queue is full.
bool blocked_on_queue_;
/// IO buffers that are queued for this scan range.
/// Condition variable for GetNext
boost::condition_variable buffer_ready_cv_;
std::list<BufferDescriptor*> ready_buffers_;
/// The soft capacity limit for ready_buffers_. ready_buffers_ can exceed
/// the limit temporarily as the capacity is adjusted dynamically.
/// In that case, the capcity is only realized when the caller removes buffers
/// from ready_buffers_.
int ready_buffers_capacity_;
/// Lock that should be taken during hdfs calls. Only one thread (the disk reading
/// thread) calls into hdfs at a time so this lock does not have performance impact.
/// This lock only serves to coordinate cleanup. Specifically it serves to ensure
/// that the disk threads are finished with HDFS calls before is_cancelled_ is set
/// to true and cleanup starts.
/// If this lock and lock_ need to be taken, lock_ must be taken first.
boost::mutex hdfs_lock_;
/// If true, this scan range has been cancelled.
bool is_cancelled_;
/// Last modified time of the file associated with the scan range
int64_t mtime_;
};
/// Used to specify data to be written to a file and offset.
/// It is the responsibility of the client to ensure that the data to be written is
/// valid and that the file to be written to exists until the callback is invoked.
/// A callback is invoked to inform the client when the write is done.
class WriteRange : public RequestRange {
public:
/// This callback is invoked on each WriteRange after the write is complete or the
/// context is cancelled. The status returned by the callback parameter indicates
/// if the write was successful (i.e. Status::OK), if there was an error
/// TStatusCode::RUNTIME_ERROR) or if the context was cancelled
/// (TStatusCode::CANCELLED). The callback is only invoked if this WriteRange was
/// successfully added (i.e. AddWriteRange() succeeded). No locks are held while
/// the callback is invoked.
typedef std::function<void(const Status&)> WriteDoneCallback;
WriteRange(const std::string& file, int64_t file_offset, int disk_id,
WriteDoneCallback callback);
/// Change the file and offset of this write range. Data and callbacks are unchanged.
/// Can only be called when the write is not in flight (i.e. before AddWriteRange()
/// is called or after the write callback was called).
void SetRange(const std::string& file, int64_t file_offset, int disk_id);
/// Set the data and number of bytes to be written for this WriteRange.
/// Can only be called when the write is not in flight (i.e. before AddWriteRange()
/// is called or after the write callback was called).
void SetData(const uint8_t* buffer, int64_t len);
const uint8_t* data() const { return data_; }
private:
friend class DiskIoMgr;
friend class DiskIoRequestContext;
/// Data to be written. RequestRange::len_ contains the length of data
/// to be written.
const uint8_t* data_;
/// Callback to invoke after the write is complete.
WriteDoneCallback callback_;
};
/// Create a DiskIoMgr object.
/// - num_disks: The number of disks the IoMgr should use. This is used for testing.
/// Specify 0, to have the disk IoMgr query the os for the number of disks.
/// - threads_per_disk: number of read threads to create per disk. This is also
/// the max queue depth.
/// - min_buffer_size: minimum io buffer size (in bytes)
/// - max_buffer_size: maximum io buffer size (in bytes). Also the max read size.
DiskIoMgr(int num_disks, int threads_per_disk, int min_buffer_size,
int max_buffer_size);
/// Create DiskIoMgr with default configs.
DiskIoMgr();
/// Clean up all threads and resources. This is mostly useful for testing since
/// for impalad, this object is never destroyed.
~DiskIoMgr();
/// Initialize the IoMgr. Must be called once before any of the other APIs.
Status Init(MemTracker* process_mem_tracker) WARN_UNUSED_RESULT;
/// Allocates tracking structure for a request context.
/// Register a new request context which is returned in *request_context.
/// The IoMgr owns the allocated DiskIoRequestContext object. The caller must call
/// UnregisterContext() for each context.
/// reader_mem_tracker: Is non-null only for readers. IO buffers
/// used for this reader will be tracked by this. If the limit is exceeded
/// the reader will be cancelled and MEM_LIMIT_EXCEEDED will be returned via
/// GetNext().
void RegisterContext(DiskIoRequestContext** request_context,
MemTracker* reader_mem_tracker);
/// Unregisters context from the disk IoMgr. This must be called for every
/// RegisterContext() regardless of cancellation and must be called in the
/// same thread as GetNext()
/// The 'context' cannot be used after this call.
/// This call blocks until all the disk threads have finished cleaning up.
/// UnregisterContext also cancels the reader/writer from the disk IoMgr.
void UnregisterContext(DiskIoRequestContext* context);
/// This function cancels the context asychronously. All outstanding requests
/// are aborted and tracking structures cleaned up. This does not need to be
/// called if the context finishes normally.
/// This will also fail any outstanding GetNext()/Read requests.
/// If wait_for_disks_completion is true, wait for the number of active disks for this
/// context to reach 0. After calling with wait_for_disks_completion = true, the only
/// valid API is returning IO buffers that have already been returned.
/// Takes context->lock_ if wait_for_disks_completion is true.
void CancelContext(DiskIoRequestContext* context, bool wait_for_disks_completion = false);
/// Adds the scan ranges to the queues. This call is non-blocking. The caller must
/// not deallocate the scan range pointers before UnregisterContext().
/// If schedule_immediately, the ranges are immediately put on the read queue
/// (i.e. the caller should not/cannot call GetNextRange for these ranges).
/// This can be used to do synchronous reads as well as schedule dependent ranges,
/// as in the case for columnar formats.
Status AddScanRanges(DiskIoRequestContext* reader,
const std::vector<ScanRange*>& ranges,
bool schedule_immediately = false) WARN_UNUSED_RESULT;
Status AddScanRange(DiskIoRequestContext* reader, ScanRange* range,
bool schedule_immediately = false) WARN_UNUSED_RESULT;
/// Add a WriteRange for the writer. This is non-blocking and schedules the context
/// on the IoMgr disk queue. Does not create any files.
Status AddWriteRange(
DiskIoRequestContext* writer, WriteRange* write_range) WARN_UNUSED_RESULT;
/// Returns the next unstarted scan range for this reader. When the range is returned,
/// the disk threads in the IoMgr will already have started reading from it. The
/// caller is expected to call ScanRange::GetNext on the returned range.
/// If there are no more unstarted ranges, NULL is returned.
/// This call is blocking.
Status GetNextRange(DiskIoRequestContext* reader, ScanRange** range) WARN_UNUSED_RESULT;
/// Reads the range and returns the result in buffer.
/// This behaves like the typical synchronous read() api, blocking until the data
/// is read. This can be called while there are outstanding ScanRanges and is
/// thread safe. Multiple threads can be calling Read() per reader at a time.
/// range *cannot* have already been added via AddScanRanges.
/// This can only be used if the scan range fits in a single IO buffer (i.e. is smaller
/// than max_read_buffer_size()) or if reading into a client-provided buffer.
Status Read(DiskIoRequestContext* reader, ScanRange* range,
BufferDescriptor** buffer) WARN_UNUSED_RESULT;
/// Determine which disk queue this file should be assigned to. Returns an index into
/// disk_queues_. The disk_id is the volume ID for the local disk that holds the
/// files, or -1 if unknown. Flag expected_local is true iff this impalad is
/// co-located with the datanode for this file.
int AssignQueue(const char* file, int disk_id, bool expected_local);
/// TODO: The functions below can be moved to DiskIoRequestContext.
/// Returns the current status of the context.
Status context_status(DiskIoRequestContext* context) const WARN_UNUSED_RESULT;
void set_bytes_read_counter(DiskIoRequestContext*, RuntimeProfile::Counter*);
void set_read_timer(DiskIoRequestContext*, RuntimeProfile::Counter*);
void set_active_read_thread_counter(DiskIoRequestContext*, RuntimeProfile::Counter*);
void set_disks_access_bitmap(DiskIoRequestContext*, RuntimeProfile::Counter*);
int64_t queue_size(DiskIoRequestContext* reader) const;
int64_t bytes_read_local(DiskIoRequestContext* reader) const;
int64_t bytes_read_short_circuit(DiskIoRequestContext* reader) const;
int64_t bytes_read_dn_cache(DiskIoRequestContext* reader) const;
int num_remote_ranges(DiskIoRequestContext* reader) const;
int64_t unexpected_remote_bytes(DiskIoRequestContext* reader) const;
/// Returns the read throughput across all readers.
/// TODO: should this be a sliding window? This should report metrics for the
/// last minute, hour and since the beginning.
int64_t GetReadThroughput();
/// Returns the maximum read buffer size
int max_read_buffer_size() const { return max_buffer_size_; }
/// Returns the total number of disk queues (both local and remote).
int num_total_disks() const { return disk_queues_.size(); }
/// Returns the total number of remote "disk" queues.
int num_remote_disks() const { return REMOTE_NUM_DISKS; }
/// Returns the number of local disks attached to the system.
int num_local_disks() const { return num_total_disks() - num_remote_disks(); }
/// The disk ID (and therefore disk_queues_ index) used for DFS accesses.
int RemoteDfsDiskId() const { return num_local_disks() + REMOTE_DFS_DISK_OFFSET; }
/// The disk ID (and therefore disk_queues_ index) used for S3 accesses.
int RemoteS3DiskId() const { return num_local_disks() + REMOTE_S3_DISK_OFFSET; }
/// The disk ID (and therefore disk_queues_ index) used for ADLS accesses.
int RemoteAdlsDiskId() const { return num_local_disks() + REMOTE_ADLS_DISK_OFFSET; }
/// Dumps the disk IoMgr queues (for readers and disks)
std::string DebugString();
/// Validates the internal state is consistent. This is intended to only be used
/// for debugging.
bool Validate() const;
/// Given a FS handle, name and last modified time of the file, tries to open that file
/// and return an instance of HdfsCachedFileHandle. In case of an error returns NULL.
HdfsCachedFileHandle* OpenHdfsFile(const hdfsFS& fs, const char* fname, int64_t mtime);
/// When the file handle is no longer in use by the scan range, return it and try to
/// unbuffer the handle. If unbuffering, closing sockets and dropping buffers in the
/// libhdfs client, is not supported, close the file handle. If the unbuffer operation
/// is supported, put the file handle together with the mtime in the LRU cache for
/// later reuse.
void CacheOrCloseFileHandle(const char* fname, HdfsCachedFileHandle* fid, bool close);
/// Garbage collect unused I/O buffers up to 'bytes_to_free', or all the buffers if
/// 'bytes_to_free' is -1.
void GcIoBuffers(int64_t bytes_to_free = -1);
/// Default ready buffer queue capacity. This constant doesn't matter too much
/// since the system dynamically adjusts.
static const int DEFAULT_QUEUE_CAPACITY;
/// "Disk" queue offsets for remote accesses. Offset 0 corresponds to
/// disk ID (i.e. disk_queue_ index) of num_local_disks().
enum {
REMOTE_DFS_DISK_OFFSET = 0,
REMOTE_S3_DISK_OFFSET,
REMOTE_ADLS_DISK_OFFSET,
REMOTE_NUM_DISKS
};
private:
friend class BufferDescriptor;
friend class DiskIoRequestContext;
struct DiskQueue;
class RequestContextCache;
friend class DiskIoMgrTest_Buffers_Test;
/// Pool to allocate BufferDescriptors.
ObjectPool pool_;
/// Memory tracker for unused I/O buffers owned by DiskIoMgr.
boost::scoped_ptr<MemTracker> free_buffer_mem_tracker_;
/// Memory tracker for I/O buffers where the DiskIoRequestContext has no MemTracker.
/// TODO: once IMPALA-3200 is fixed, there should be no more cases where readers don't
/// provide a MemTracker.
boost::scoped_ptr<MemTracker> unowned_buffer_mem_tracker_;
/// Number of worker(read) threads per disk. Also the max depth of queued
/// work to the disk.
const int num_threads_per_disk_;
/// Maximum read size. This is also the maximum size of each allocated buffer.
const int max_buffer_size_;
/// The minimum size of each read buffer.
const int min_buffer_size_;
/// Thread group containing all the worker threads.
ThreadGroup disk_thread_group_;
/// Options object for cached hdfs reads. Set on startup and never modified.
struct hadoopRzOptions* cached_read_options_;
/// True if the IoMgr should be torn down. Worker threads watch for this to
/// know to terminate. This variable is read/written to by different threads.
volatile bool shut_down_;
/// Total bytes read by the IoMgr.
RuntimeProfile::Counter total_bytes_read_counter_;
/// Total time spent in hdfs reading
RuntimeProfile::Counter read_timer_;
/// Contains all contexts that the IoMgr is tracking. This includes contexts that are
/// active as well as those in the process of being cancelled. This is a cache
/// of context objects that get recycled to minimize object allocations and lock
/// contention.
boost::scoped_ptr<RequestContextCache> request_context_cache_;
/// Protects free_buffers_ and free_buffer_descs_
boost::mutex free_buffers_lock_;
/// Free buffers that can be handed out to clients. There is one list for each buffer
/// size, indexed by the Log2 of the buffer size in units of min_buffer_size_. The
/// maximum buffer size is max_buffer_size_, so the maximum index is
/// Log2(max_buffer_size_ / min_buffer_size_).
//
/// E.g. if min_buffer_size_ = 1024 bytes:
/// free_buffers_[0] => list of free buffers with size 1024 B
/// free_buffers_[1] => list of free buffers with size 2048 B
/// free_buffers_[10] => list of free buffers with size 1 MB
/// free_buffers_[13] => list of free buffers with size 8 MB
/// free_buffers_[n] => list of free buffers with size 2^n * 1024 B
std::vector<std::list<uint8_t*>> free_buffers_;
/// List of free buffer desc objects that can be handed out to clients
std::list<BufferDescriptor*> free_buffer_descs_;
/// Total number of allocated buffers, used for debugging.
AtomicInt32 num_allocated_buffers_;
/// Total number of buffers in readers
AtomicInt32 num_buffers_in_readers_;
/// Per disk queues. This is static and created once at Init() time. One queue is
/// allocated for each local disk on the system and for each remote filesystem type.
/// It is indexed by disk id.
std::vector<DiskQueue*> disk_queues_;
/// The next disk queue to write to if the actual 'disk_id_' is unknown (i.e. the file
/// is not associated with a particular local disk or remote queue). Used to implement
/// round-robin assignment for that case.
static AtomicInt32 next_disk_id_;
// Caching structure that maps file names to cached file handles. The cache has an upper
// limit of entries defined by FLAGS_max_cached_file_handles. Evicted cached file
// handles are closed.
FifoMultimap<std::string, HdfsCachedFileHandle*> file_handle_cache_;
/// Returns the index into free_buffers_ for a given buffer size
int free_buffers_idx(int64_t buffer_size);
/// Returns a buffer to read into with size between 'buffer_size' and
/// 'max_buffer_size_', If there is an appropriately-sized free buffer in the
/// 'free_buffers_', that is returned, otherwise a new one is allocated.
/// The returned *buffer_size must be between 0 and 'max_buffer_size_'.
/// The buffer memory is tracked against reader's mem tracker, or
/// 'unowned_buffer_mem_tracker_' if the reader does not have one.
BufferDescriptor* GetFreeBuffer(DiskIoRequestContext* reader, ScanRange* range,
int64_t buffer_size);
/// Gets a BufferDescriptor initialized with the provided parameters. The object may be
/// recycled or newly allocated. Does not do anything aside from initialize the
/// descriptor's fields.
BufferDescriptor* GetBufferDesc(DiskIoRequestContext* reader, MemTracker* mem_tracker,
ScanRange* range, uint8_t* buffer, int64_t buffer_size);
/// Returns the buffer desc and underlying buffer to the disk IoMgr. This also updates
/// the reader and disk queue state.
void ReturnBuffer(BufferDescriptor* buffer);
/// Returns a buffer desc object which can now be used for another reader.
void ReturnBufferDesc(BufferDescriptor* desc);
/// Disassociates the desc->buffer_ memory from 'desc' (which cannot be NULL), either
/// freeing it or returning it to 'free_buffers_'. Memory tracking is updated to
/// reflect the transfer of ownership from desc->mem_tracker_ to the disk I/O mgr.
void FreeBufferMemory(BufferDescriptor* desc);
/// Disk worker thread loop. This function retrieves the next range to process on
/// the disk queue and invokes ReadRange() or Write() depending on the type of Range().
/// There can be multiple threads per disk running this loop.
void WorkLoop(DiskQueue* queue);
/// This is called from the disk thread to get the next range to process. It will
/// wait until a scan range and buffer are available, or a write range is available.
/// This functions returns the range to process.
/// Only returns false if the disk thread should be shut down.
/// No locks should be taken before this function call and none are left taken after.
bool GetNextRequestRange(DiskQueue* disk_queue, RequestRange** range,
DiskIoRequestContext** request_context);
/// Updates disk queue and reader state after a read is complete. The read result
/// is captured in the buffer descriptor.
void HandleReadFinished(DiskQueue*, DiskIoRequestContext*, BufferDescriptor*);
/// Invokes write_range->callback_ after the range has been written and
/// updates per-disk state and handle state. The status of the write OK/RUNTIME_ERROR
/// etc. is passed via write_status and to the callback.
/// The write_status does not affect the writer->status_. That is, an write error does
/// not cancel the writer context - that decision is left to the callback handler.
/// TODO: On the read path, consider not canceling the reader context on error.
void HandleWriteFinished(
DiskIoRequestContext* writer, WriteRange* write_range, const Status& write_status);
/// Validates that range is correctly initialized
Status ValidateScanRange(ScanRange* range) WARN_UNUSED_RESULT;
/// Write the specified range to disk and calls HandleWriteFinished when done.
/// Responsible for opening and closing the file that is written.
void Write(DiskIoRequestContext* writer_context, WriteRange* write_range);
/// Helper method to write a range using the specified FILE handle. Returns Status:OK
/// if the write succeeded, or a RUNTIME_ERROR with an appropriate message otherwise.
/// Does not open or close the file that is written.
Status WriteRangeHelper(FILE* file_handle, WriteRange* write_range) WARN_UNUSED_RESULT;
/// Reads the specified scan range and calls HandleReadFinished when done.
void ReadRange(DiskQueue* disk_queue, DiskIoRequestContext* reader, ScanRange* range);
/// Try to allocate the next buffer for the scan range, returning the new buffer
/// if successful. If 'reader' is cancelled, cancels the range and returns NULL.
/// If there is memory pressure and buffers are already queued, adds the range
/// to the blocked ranges and returns NULL.
BufferDescriptor* TryAllocateNextBufferForRange(DiskQueue* disk_queue,
DiskIoRequestContext* reader, ScanRange* range, int64_t buffer_size);
};
}
#endif