| // 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. |
| |
| #include "runtime/io/data-cache.h" |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <mutex> |
| #include <string.h> |
| #include <unistd.h> |
| #include <sstream> |
| #include <fstream> |
| #include <unordered_map> |
| #include <unordered_set> |
| #include <sys/stat.h> |
| |
| #include <glog/logging.h> |
| #include <boost/archive/binary_oarchive.hpp> |
| #include <boost/archive/binary_iarchive.hpp> |
| #include <boost/serialization/access.hpp> |
| #include <boost/serialization/vector.hpp> |
| #include <boost/thread/pthread/shared_mutex.hpp> |
| |
| #include "common/compiler-util.h" |
| #include "exec/kudu/kudu-util.h" |
| #include "kudu/util/env.h" |
| #include "kudu/util/locks.h" |
| #include "kudu/util/path_util.h" |
| #include "gutil/port.h" |
| #include "gutil/strings/split.h" |
| #include "gutil/walltime.h" |
| #include "runtime/io/data-cache-trace.h" |
| #include "util/bit-util.h" |
| #include "util/cache/cache.h" |
| #include "util/disk-info.h" |
| #include "util/error-util.h" |
| #include "util/filesystem-util.h" |
| #include "util/hash-util.h" |
| #include "util/histogram-metric.h" |
| #include "util/impalad-metrics.h" |
| #include "util/metrics.h" |
| #include "util/parse-util.h" |
| #include "util/pretty-printer.h" |
| #include "util/scope-exit-trigger.h" |
| #include "util/test-info.h" |
| #include "util/uid-util.h" |
| |
| #ifndef FALLOC_FL_PUNCH_HOLE |
| #include <linux/falloc.h> |
| #endif |
| |
| #include "common/names.h" |
| |
| using kudu::Env; |
| using kudu::faststring; |
| using kudu::JoinPathSegments; |
| using kudu::percpu_rwlock; |
| using kudu::RWFile; |
| using kudu::rw_spinlock; |
| using kudu::Slice; |
| using kudu::WritableFile; |
| using strings::SkipEmpty; |
| using strings::Split; |
| using boost::shared_mutex; |
| |
| #ifdef NDEBUG |
| #define ENABLE_CHECKSUMMING (false) |
| #else |
| #define ENABLE_CHECKSUMMING (true) |
| #endif |
| |
| // Rotational disks should have 1 thread per disk to minimize seeks. Non-rotational |
| // don't have this penalty and benefit from multiple concurrent IO requests. |
| static const int THREADS_PER_ROTATIONAL_DISK = 1; |
| static const int THREADS_PER_SOLID_STATE_DISK = 8; |
| |
| DEFINE_int64(data_cache_file_max_size_bytes, 1L << 40 /* 1TB */, |
| "(Advanced) The maximum size which a cache file can grow to before data stops being " |
| "appended to it."); |
| DEFINE_int32(data_cache_max_opened_files, 1000, |
| "(Advanced) The maximum number of allowed opened files. This must be at least the " |
| "number of specified partitions."); |
| static const std::string data_cache_write_concurrency_help_msg = Substitute("(Advanced) " |
| "Number of concurrent threads allowed to insert into the cache per partition; unset " |
| "uses $0 for rotational disks and $1 for solid state disks.", |
| THREADS_PER_ROTATIONAL_DISK, THREADS_PER_SOLID_STATE_DISK); |
| DEFINE_int32(data_cache_write_concurrency, 0, |
| data_cache_write_concurrency_help_msg.c_str()); |
| DEFINE_bool(data_cache_checksum, ENABLE_CHECKSUMMING, |
| "(Advanced) Enable checksumming for the cached buffer."); |
| |
| DEFINE_bool(data_cache_enable_tracing, false, |
| "(Advanced) Collect a trace of all lookups in the data cache."); |
| DEFINE_bool(data_cache_anonymize_trace, false, |
| "(Advanced) Use hashes of filenames rather than file paths in the data " |
| "cache access trace."); |
| DEFINE_int32(data_cache_trace_percentage, 100, "The percentage of cache lookups that " |
| "should be emitted to the trace file."); |
| DECLARE_string(log_dir); |
| DEFINE_string(data_cache_trace_dir, "", "The base directory for data cache tracing. " |
| "The data cache trace files for each cache directory are placed in separate " |
| "subdirectories underneath this base directory. If blank, defaults to " |
| "<log_file_dir>/data_cache_trace/"); |
| DEFINE_int32(max_data_cache_trace_file_size, 100000, "The maximum size (in " |
| "log entries) of the data cache trace file before a new one is created."); |
| DEFINE_int32(max_data_cache_trace_files, 10, "Maximum number of data cache trace " |
| "files to retain for each cache directory specified by the data_cache startup " |
| "parameter. The most recent trace files are retained. If set to 0, all trace files " |
| "are retained."); |
| |
| DEFINE_string(data_cache_eviction_policy, "LRU", |
| "(Advanced) The cache eviction policy to use for the data cache. " |
| "Either 'LRU' (default) or 'LIRS' (experimental)"); |
| |
| DEFINE_string(data_cache_async_write_buffer_limit, "1GB", |
| "(Experimental) Limit of the total buffer size used by asynchronous store tasks."); |
| |
| DECLARE_bool(data_cache_keep_across_restarts); |
| |
| namespace impala { |
| namespace io { |
| |
| static const int64_t PAGE_SIZE = 1L << 12; |
| const char* DataCache::Partition::CACHE_FILE_PREFIX = "impala-cache-file-"; |
| const char* DataCache::Partition::DUMP_FILE_NAME = "impala-cache-metadata"; |
| const int MAX_FILE_DELETER_QUEUE_SIZE = 500; |
| |
| // This large value for the queue size is harmless because the total size of the entries |
| // on the queue are bound by --data_cache_async_write_bufer_limit. |
| const int MAX_STORE_TASK_QUEUE_SIZE = 1 << 20; |
| |
| static const char* PARTITION_PATH_METRIC_KEY_TEMPLATE = |
| "impala-server.io-mgr.remote-data-cache-partition-$0.path"; |
| static const char* PARTITION_READ_LATENCY_METRIC_KEY_TEMPLATE = |
| "impala-server.io-mgr.remote-data-cache-partition-$0.read-latency"; |
| static const char* PARTITION_WRITE_LATENCY_METRIC_KEY_TEMPLATE = |
| "impala-server.io-mgr.remote-data-cache-partition-$0.write-latency"; |
| static const char* PARTITION_EVICTION_LATENCY_METRIC_KEY_TEMPLATE = |
| "impala-server.io-mgr.remote-data-cache-partition-$0.eviction-latency"; |
| |
| |
| /// This class is an implementation of backing files in a cache partition. |
| /// |
| /// A partition uses the interface Create() to create a backing file. A reader can read |
| /// from the backing file using the interface Read(). |
| /// |
| /// The backing file is append-only. To insert new data into the file, Allocate() is |
| /// called to reserve a contiguous area in the backing file. If the reservation succeeds, |
| /// the insertion offset is returned. Write() is called to add the data at the insertion |
| /// offset in the backing file. Allocations in the file may be evicted by punching hole |
| /// (via PunchHole()) in the backing file. The data in the hole area is reclaimed by the |
| /// underlying filesystem. |
| /// |
| /// To avoid having too many backing files opened, old files are deleted to keep the |
| /// number of opened files within --data_cache_max_opened_files. Files are deleted |
| /// asynchronously by the file deleter thread pool. To synchronize between file deletion |
| /// and concurrent accesses of the file via Read()/Write()/PunchHole(), reader lock |
| /// is held in those functions. Before a file is deleted, Close() must be called by |
| /// the deleter thread, which holds the writer lock to block off all readers and sets |
| /// 'file_' to NULL. Read()/Write()/PunchHole() will check whether 'file_' is NULL. |
| /// If the file is already closed, the function will fail. On a failure of Read(), the |
| /// caller is expected to delete the stale cache entry. On a failure of Write(), the |
| /// caller is not expected to insert the cache entry. In other words, any stale cache |
| /// entry which references a deleted file will either be lazily erased on Read() or |
| /// evicted due to inactivity. |
| /// |
| class DataCache::CacheFile { |
| public: |
| ~CacheFile() { |
| // Close file if it's not closed already. |
| DeleteFile(); |
| } |
| |
| static Status Create(std::string path, std::unique_ptr<CacheFile>* cache_file_ptr) { |
| unique_ptr<CacheFile> cache_file(new CacheFile(move(path))); |
| KUDU_RETURN_IF_ERROR( |
| kudu::Env::Default()->NewRWFile(cache_file->path_, &cache_file->file_), |
| "Failed to create cache file"); |
| *cache_file_ptr = std::move(cache_file); |
| return Status::OK(); |
| } |
| |
| static Status Open(std::string path, bool allow_append, int64_t current_offset, |
| std::unique_ptr<CacheFile>* cache_file_ptr) { |
| unique_ptr<CacheFile> cache_file(new CacheFile(move(path))); |
| kudu::RWFileOptions opts; |
| opts.mode = kudu::Env::OpenMode::MUST_EXIST; |
| KUDU_RETURN_IF_ERROR( |
| kudu::Env::Default()->NewRWFile(opts, cache_file->path_, &cache_file->file_), |
| "Failed to open cache file"); |
| cache_file->allow_append_ = allow_append; |
| cache_file->current_offset_.Store(current_offset); |
| *cache_file_ptr = std::move(cache_file); |
| return Status::OK(); |
| } |
| |
| // Close the underlying file so it cannot be read or written to anymore. |
| void Close() { |
| // Explicitly hold the lock in write mode to block all readers. This ensures that |
| // setting 'file_' to NULL and 'allow_append_' to false below is atomic. |
| std::unique_lock<percpu_rwlock> lock(lock_); |
| // If the file is already closed, nothing to do. |
| if (!file_) return; |
| kudu::Status status = file_->Close(); |
| if (!status.ok()) { |
| LOG(WARNING) << Substitute("Failed to close cache file $0: $1", path_, |
| status.ToString()); |
| } |
| file_.reset(); |
| allow_append_ = false; |
| } |
| |
| // Close the underlying file and delete it from the filesystem. |
| void DeleteFile() { |
| Close(); |
| if (readonly_.Load()) return; |
| DCHECK(!file_); |
| kudu::Status status = kudu::Env::Default()->DeleteFile(path_); |
| if (!status.ok()) { |
| LOG(WARNING) << Substitute("Failed to unlink $0: $1", path_, status.ToString()); |
| } |
| } |
| |
| // Allocates a chunk of 'len' bytes in this file. The cache partition's lock |
| // 'partition_lock' must be held when calling this function. Returns the byte offset |
| // into the file for insertion. 'len' is expected to be multiples of PAGE_SIZE. |
| // Returns -1 if the file doesn't have enough space for insertion. |
| int64_t Allocate(int64_t len, const std::unique_lock<SpinLock>& partition_lock) { |
| DCHECK(partition_lock.owns_lock()); |
| DCHECK_EQ(len % PAGE_SIZE, 0); |
| int64_t current_offset = current_offset_.Load(); |
| DCHECK_EQ(current_offset % PAGE_SIZE, 0); |
| // Hold the lock in shared mode to check if 'file_' is not closed already. |
| kudu::shared_lock<rw_spinlock> lock(lock_.get_lock()); |
| if (!allow_append_ || (current_offset + len > FLAGS_data_cache_file_max_size_bytes && |
| current_offset > 0)) { |
| allow_append_ = false; |
| return -1; |
| } |
| DCHECK(file_); |
| int64_t insertion_offset = current_offset; |
| current_offset_.Add(len); |
| return insertion_offset; |
| } |
| |
| // Reads from byte offset 'offset' for 'bytes_to_read' bytes into 'buffer'. |
| // Returns true iff read succeeded. Returns false on error or if the file |
| // is already closed. |
| bool Read(int64_t offset, uint8_t* buffer, int64_t bytes_to_read) { |
| DCHECK_EQ(offset % PAGE_SIZE, 0); |
| // Hold the lock in shared mode to check if 'file_' is not closed already. |
| kudu::shared_lock<rw_spinlock> lock(lock_.get_lock()); |
| if (UNLIKELY(!file_)) return false; |
| DCHECK_LE(offset + bytes_to_read, current_offset_.Load()); |
| kudu::Status status = file_->Read(offset, Slice(buffer, bytes_to_read)); |
| if (UNLIKELY(!status.ok())) { |
| LOG(ERROR) << Substitute("Failed to read from $0 at offset $1 for $2 bytes: $3", |
| path_, offset, PrettyPrinter::PrintBytes(bytes_to_read), status.ToString()); |
| return false; |
| } |
| return true; |
| } |
| |
| // Writes 'buffer' of length 'buffer_len' into byte offset 'offset' in the file. |
| // Returns true iff write succeeded. Returns false on errors or if the file is |
| // already closed. |
| bool Write(int64_t offset, const uint8_t* buffer, int64_t buffer_len) { |
| DCHECK_EQ(offset % PAGE_SIZE, 0); |
| DCHECK_LE(offset, current_offset_.Load()); |
| if (UNLIKELY(readonly_.Load())) return false; |
| // Hold the lock in shared mode to check if 'file_' is not closed already. |
| kudu::shared_lock<rw_spinlock> lock(lock_.get_lock()); |
| if (UNLIKELY(!file_ || readonly_.Load())) return false; |
| DCHECK_LE(offset + buffer_len, current_offset_.Load()); |
| kudu::Status status = file_->Write(offset, Slice(buffer, buffer_len)); |
| if (UNLIKELY(!status.ok())) { |
| LOG(ERROR) << Substitute("Failed to write to $0 at offset $1 for $2 bytes: $3", |
| path_, offset, PrettyPrinter::PrintBytes(buffer_len), status.ToString()); |
| return false; |
| } |
| return true; |
| } |
| |
| void PunchHole(int64_t offset, int64_t hole_size) { |
| DCHECK_EQ(offset % PAGE_SIZE, 0); |
| DCHECK_EQ(hole_size % PAGE_SIZE, 0); |
| if (UNLIKELY(readonly_.Load())) return; |
| // Hold the lock in shared mode to check if 'file_' is not closed already. |
| kudu::shared_lock<rw_spinlock> lock(lock_.get_lock()); |
| if (UNLIKELY(!file_ || readonly_.Load())) return; |
| DCHECK_LE(offset + hole_size, current_offset_.Load()); |
| kudu::Status status = file_->PunchHole(offset, hole_size); |
| if (UNLIKELY(!status.ok())) { |
| LOG(DFATAL) << Substitute("Failed to punch hole in $0 at offset $1 for $2 $3", |
| path_, offset, PrettyPrinter::PrintBytes(hole_size), status.ToString()); |
| } |
| } |
| |
| Status Flush() { |
| std::unique_lock<percpu_rwlock> lock(lock_); |
| // If the file is already closed, nothing to do. |
| if (!file_) return Status::OK(); |
| kudu::Status status = file_->Sync(); |
| if (UNLIKELY(!status.ok())) { |
| return Status(status.message().ToString()); |
| } |
| return Status::OK(); |
| } |
| |
| void SetReadOnly() { |
| // Setting this without holding a lock tells other threads not to start new |
| // modifications. |
| readonly_.Store(true); |
| // Other threads get lock_ in shared mode for critical sections. Getting this |
| // in exclusive mode forces this thread to wait until all other threads have |
| // left their critical sections. That means that no writes should happen after |
| // this function returns (until RevokeReadOnly() runs). |
| std::unique_lock<percpu_rwlock> lock(lock_); |
| } |
| |
| void RevokeReadOnly() { |
| // Going from readonly to read/write doesn't need a full lock |
| readonly_.Store(false); |
| } |
| |
| int64_t mtime() { |
| // If the file is already closed, don't care about its mtime. |
| if (!file_) return -1; |
| struct stat buf; |
| stat(path_.c_str(), &buf); |
| return buf.st_mtime; |
| } |
| |
| const string& path() const { return path_; } |
| |
| bool closed() const { return file_ == nullptr; } |
| |
| bool allow_append() const { return allow_append_; } |
| |
| int64_t current_offset() const { return current_offset_.Load(); } |
| |
| private: |
| /// Full path of the backing file in the local storage. |
| const string path_; |
| |
| /// The underlying backing file. NULL if the file has been closed. |
| unique_ptr<RWFile> file_; |
| |
| /// True iff it's okay to append to this backing file. |
| bool allow_append_ = true; |
| |
| /// The backing file cannot be written or punched hole if this is true. |
| AtomicBool readonly_{false}; |
| |
| /// The current offset in the file to append to on next insert. |
| AtomicInt64 current_offset_; |
| |
| /// This is a reader-writer lock used for synchronization with the deleter thread. |
| /// It is taken in write mode in Close()/SetReadOnly() and shared mode |
| /// everywhere else. It's expected that all places except for |
| /// Close()/SetReadOnly() check that 'file_' is not NULL with the lock held in |
| /// shared mode while Close() ensures that no thread is holding the lock in |
| /// shared mode so it's safe to close the file. The file can no longer be read, written |
| /// or hole punched after it has been closed. The only operation allowed is deletion. |
| percpu_rwlock lock_; |
| |
| /// C'tor of CacheFile to be called by Create() only. |
| explicit CacheFile(std::string path) : path_(move(path)) { } |
| |
| DISALLOW_COPY_AND_ASSIGN(CacheFile); |
| }; |
| |
| /// An entry in the metadata cache in a partition. |
| /// Contains the whereabouts of the cached content. |
| class DataCache::CacheEntry { |
| public: |
| explicit CacheEntry(CacheFile* file, int64_t offset, int64_t len, uint64_t checksum) |
| : file_(file), offset_(offset), len_(len), checksum_(checksum) { |
| } |
| |
| // Unpack a cache's entry represented by 'slice'. This is done in place of casting |
| // to avoid any potential alignment issue. |
| explicit CacheEntry(const Slice& value) { |
| DCHECK_EQ(value.size(), sizeof(CacheEntry)); |
| memcpy(this, value.data(), value.size()); |
| } |
| |
| CacheFile* file() const { return file_; } |
| int64_t offset() const { return offset_; } |
| int64_t len() const { return len_; } |
| uint64_t checksum() const { return checksum_; } |
| |
| private: |
| /// The backing file holding the cached content. |
| CacheFile* const file_ = nullptr; |
| |
| /// The starting byte offset in the backing file at which the content is stored. |
| const int64_t offset_ = 0; |
| |
| /// The length in bytes of the cached content. |
| const int64_t len_ = 0; |
| |
| /// Optional checksum of the content computed when inserting the cache entry. |
| const uint64_t checksum_ = 0; |
| }; |
| |
| /// The key used for look up in the cache. |
| struct DataCache::CacheKey { |
| public: |
| explicit CacheKey(const string& filename, int64_t mtime, int64_t offset) |
| : key_(filename.size() + sizeof(mtime) + sizeof(offset)) { |
| DCHECK_GE(mtime, 0); |
| DCHECK_GE(offset, 0); |
| key_.append(&mtime, sizeof(mtime)); |
| key_.append(&offset, sizeof(offset)); |
| key_.append(filename); |
| } |
| |
| int64_t Hash() const { |
| return HashUtil::FastHash64(key_.data(), key_.size(), 0); |
| } |
| |
| Slice filename() const { |
| return Slice(key_.data() + OFFSETOF_FILENAME, key_.size() - OFFSETOF_FILENAME); |
| } |
| |
| int64_t mtime() const { |
| return UNALIGNED_LOAD64(key_.data() + OFFSETOF_MTIME); |
| } |
| |
| int64_t offset() const { |
| return UNALIGNED_LOAD64(key_.data() + OFFSETOF_OFFSET); |
| } |
| |
| Slice ToSlice() const { |
| return key_; |
| } |
| |
| private: |
| // Key encoding stored in key_: |
| // |
| // int64_t mtime; |
| // int64_t offset; |
| // <variable length bytes> filename; |
| static constexpr int OFFSETOF_MTIME = 0; |
| static constexpr int OFFSETOF_OFFSET = OFFSETOF_MTIME + sizeof(int64_t); |
| static constexpr int OFFSETOF_FILENAME = OFFSETOF_OFFSET + sizeof(int64_t); |
| faststring key_; |
| }; |
| |
| /// The helper class to dump/load cache metadata. DumpData is the intermediary between |
| /// cache metadata and dump file. Data cache partition can push data into DumpData and |
| /// serialize it to disk or deserialize a DumpData from disk to load the metadata. |
| class DataCache::Partition::DumpData { |
| public: |
| |
| void serialize(std::ofstream& os) { |
| boost::archive::binary_oarchive oa(os); |
| oa << *this; |
| } |
| |
| void deserialize(std::ifstream& is) { |
| boost::archive::binary_iarchive ia(is); |
| ia >> *this; |
| } |
| |
| private: |
| friend class Partition; |
| friend class boost::serialization::access; |
| |
| /// The dump struct of CacheFile. |
| struct CacheFileData { |
| CacheFileData() = default; |
| |
| CacheFileData(string path, bool allow_append, int64_t current_offset, int64_t mtime) |
| : path(move(path)), |
| allow_append(allow_append), |
| current_offset(current_offset), |
| mtime(mtime) {} |
| |
| template<class Archive> |
| void serialize(Archive& ar, const unsigned int version) { |
| ar & path; |
| ar & allow_append; |
| ar & current_offset; |
| ar & mtime; |
| } |
| |
| /// Corresponds to 'CacheFile::path_'. |
| string path; |
| |
| /// Corresponds to 'CacheFile::allow_append_'. |
| bool allow_append; |
| |
| /// Corresponds to 'CacheFile::current_offset_'. |
| int64_t current_offset; |
| |
| /// The last modification time of the cache file, for check when load to prevent |
| /// accidental modification of the cache file. |
| int64_t mtime; |
| }; |
| |
| /// The dump struct of a pair of CacheKey and CacheEntry. |
| struct CacheKeyEntryData { |
| CacheKeyEntryData() = default; |
| |
| CacheKeyEntryData( |
| string key, int64_t index, int64_t offset, int64_t len, uint64_t checksum) |
| : key(move(key)), index(index), offset(offset), len(len), checksum(checksum) {} |
| |
| template<class Archive> |
| void serialize(Archive& ar, const unsigned int version) { |
| ar & key; |
| ar & index; |
| ar & offset; |
| ar & len; |
| ar & checksum; |
| } |
| |
| /// Corresponds to CacheKey. |
| string key; |
| |
| /// Corresponds to CacheEntry, but uses 'index' instead of the 'file_' to point to the |
| /// cache file. When we dump the cache metadata, the 'file_' is meaningless because |
| /// the cache file objects cannot still be at the same address after reloading. So |
| /// during the dump, we use the corresponding file's index in the 'cache_files_data' |
| /// instead of file pointer, so that when reloading, the correct file pointer can be |
| /// find based on the index. |
| int64_t index; |
| int64_t offset; |
| int64_t len; |
| uint64_t checksum; |
| }; |
| |
| template<class Archive> |
| void serialize(Archive& ar, const unsigned int version) { |
| ar & cache_files_data; |
| ar & meta_cache_data; |
| } |
| |
| /// Corresponds to 'Partition::cache_files_', but does not include closed files, because |
| /// closed files can no longer be read or written, and the underlying file should have |
| /// been deleted already. |
| vector<CacheFileData> cache_files_data; |
| |
| /// Corresponds to 'Partition::meta_cache_', but does not include entries that the cache |
| /// file is closed, because these entries can no longer be hit and do not need to be |
| /// dumped. |
| vector<CacheKeyEntryData> meta_cache_data; |
| |
| /// The mapping between the cache file pointer and the index of 'cache_files_data' to |
| /// convert the file pointer to the index when the cache entry is dumped. It is built |
| /// when cache files are dumped, only used when 'meta_cache_' is dumped and will not be |
| /// dumped to disk. |
| std::unordered_map<CacheFile*, int64_t> file_indexs; |
| }; |
| |
| /// The class to abstract store behavior, holds a temporary buffer copied from the source |
| /// buffer until store complete. |
| class DataCache::StoreTask { |
| public: |
| explicit StoreTask(const std::string& filename, int64_t mtime, int64_t offset, |
| uint8_t* buffer, int64_t buffer_len, DataCache* cache) |
| : key_(filename, mtime, offset), |
| buffer_(buffer), |
| buffer_len_(buffer_len), |
| cache_(cache) { } |
| |
| ~StoreTask() { cache_->CompleteStoreTask(*this); } |
| |
| const CacheKey& key() const { return key_; } |
| const uint8_t* buffer() const { return buffer_.get(); } |
| int64_t buffer_len() const { return buffer_len_; } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(StoreTask); |
| |
| CacheKey key_; |
| std::unique_ptr<uint8_t[]> buffer_; |
| int64_t buffer_len_; |
| DataCache* cache_; |
| }; |
| |
| static Cache::EvictionPolicy GetCacheEvictionPolicy(const std::string& policy_string) { |
| Cache::EvictionPolicy policy = Cache::ParseEvictionPolicy(policy_string); |
| if (policy != Cache::EvictionPolicy::LRU && policy != Cache::EvictionPolicy::LIRS) { |
| LOG(FATAL) << "Unsupported eviction policy: " << policy_string; |
| } |
| return policy; |
| } |
| |
| static int32_t DeviceWriteConcurrency(const string& path) { |
| if (FLAGS_data_cache_write_concurrency > 0) { |
| return FLAGS_data_cache_write_concurrency; |
| } |
| |
| int path_disk_id = DiskInfo::disk_id(path.c_str()); |
| if (path_disk_id < 0) { |
| LOG(WARNING) << "Device for path " << path << " could not be determined. " |
| << "Setting data_cache_write_concurrency=" |
| << THREADS_PER_ROTATIONAL_DISK << "."; |
| return THREADS_PER_ROTATIONAL_DISK; |
| } |
| |
| const std::string& device_name = DiskInfo::device_name(path_disk_id); |
| |
| int32_t write_concurrency = THREADS_PER_ROTATIONAL_DISK; |
| const char* disk_type = "rotational"; |
| if (!DiskInfo::is_rotational(path_disk_id)) { |
| write_concurrency = THREADS_PER_SOLID_STATE_DISK; |
| disk_type = "solid state"; |
| } |
| LOG(INFO) << "Default data_cache_write_concurrency=" << write_concurrency |
| << " for " << disk_type << " disk " << device_name; |
| return write_concurrency; |
| } |
| |
| DataCache::Partition::Partition( |
| int32_t index, const string& path, int64_t capacity, int max_opened_files, |
| bool trace_replay) |
| : index_(index), |
| path_(path), |
| capacity_(max<int64_t>(capacity, PAGE_SIZE)), |
| max_opened_files_(max_opened_files), |
| trace_replay_(trace_replay), |
| meta_cache_(NewCache(GetCacheEvictionPolicy(FLAGS_data_cache_eviction_policy), |
| capacity_, path_)) {} |
| |
| DataCache::Partition::~Partition() { |
| if (!closed_) ReleaseResources(); |
| } |
| |
| Status DataCache::Partition::CreateCacheFile() { |
| DCHECK(!trace_replay_); |
| lock_.DCheckLocked(); |
| const string& path = |
| JoinPathSegments(path_, CACHE_FILE_PREFIX + PrintId(GenerateUUID())); |
| unique_ptr<CacheFile> cache_file; |
| RETURN_IF_ERROR(CacheFile::Create(path, &cache_file)); |
| cache_files_.emplace_back(std::move(cache_file)); |
| LOG(INFO) << "Created cache file " << path; |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::DeleteUntrackedFiles() const { |
| DCHECK(!trace_replay_); |
| std::unordered_set<string> tracked_file_paths; |
| for (const auto& file : cache_files_) { |
| tracked_file_paths.insert(file->path()); |
| } |
| vector<string> entries; |
| RETURN_IF_ERROR(FileSystemUtil::Directory::GetEntryNames(path_, &entries, 0, |
| FileSystemUtil::Directory::EntryType::DIR_ENTRY_REG)); |
| for (const string& entry : entries) { |
| if (entry.find(CACHE_FILE_PREFIX) == 0 || entry == DUMP_FILE_NAME) { |
| const string file_path = JoinPathSegments(path_, entry); |
| if (tracked_file_paths.find(file_path) != tracked_file_paths.end()) continue; |
| KUDU_RETURN_IF_ERROR(kudu::Env::Default()->DeleteFile(file_path), |
| Substitute("Failed to delete old cache file $0", file_path)); |
| LOG(INFO) << Substitute("Deleted old cache file $0", file_path); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::Init() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| |
| RETURN_IF_ERROR(meta_cache_->Init()); |
| |
| // Trace replay does not require further initialization, as it is only doing |
| // metadata operations and does not do filesystem operations. |
| if (trace_replay_) return Status::OK(); |
| |
| // Verify the validity of the path specified. |
| if (!FileSystemUtil::IsCanonicalPath(path_)) { |
| return Status(Substitute("$0 is not a canonical path", path_)); |
| } |
| RETURN_IF_ERROR(FileSystemUtil::VerifyIsDirectory(path_)); |
| |
| // Make sure hole punching is supported for the caching directory. |
| RETURN_IF_ERROR(FileSystemUtil::CheckHolePunch(path_)); |
| |
| if (FLAGS_data_cache_enable_tracing) { |
| if (FLAGS_data_cache_trace_percentage > 100 || |
| FLAGS_data_cache_trace_percentage < 0) { |
| return Status(Substitute("Misconfigured data_cache_trace_percentage: $0." |
| "Must be between 0 and 100.", FLAGS_data_cache_trace_percentage)); |
| } |
| |
| // If unspecified, use a directory under the "log_dir". |
| if (FLAGS_data_cache_trace_dir.empty()) { |
| stringstream default_trace_dir; |
| default_trace_dir << FLAGS_log_dir << "/data_cache_traces/"; |
| FLAGS_data_cache_trace_dir = default_trace_dir.str(); |
| } |
| // To avoid mixing trace files from different partitions, give each partition |
| // its own subdirectory. |
| stringstream trace_dir; |
| trace_dir << FLAGS_data_cache_trace_dir << Substitute("/partition-$0/", index_); |
| tracer_.reset(new trace::Tracer(trace_dir.str(), |
| FLAGS_max_data_cache_trace_file_size, FLAGS_max_data_cache_trace_files, |
| FLAGS_data_cache_anonymize_trace)); |
| RETURN_IF_ERROR(tracer_->Init()); |
| } |
| |
| data_cache_write_concurrency_ = DeviceWriteConcurrency(path_); |
| |
| // Create metrics for this partition |
| InitMetrics(); |
| |
| // Only if loading is not enabled, or if loading is enabled but fails, do we need to |
| // create new cache file. |
| if (!(FLAGS_data_cache_keep_across_restarts && Load().ok())) { |
| // Create a backing file for the partition. |
| RETURN_IF_ERROR(CreateCacheFile()); |
| oldest_opened_file_ = 0; |
| } |
| |
| // Delete all cache files that are not listed in cache_files_ (including dumped metadata |
| // file). These files were left over from previous runs and are now no longer needed. |
| RETURN_IF_ERROR(DeleteUntrackedFiles()); |
| |
| // Check if there is enough space available at this point in time. |
| int64_t used_bytes = ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_TOTAL_BYTES->GetValue(); |
| uint64_t available_bytes; |
| RETURN_IF_ERROR(FileSystemUtil::GetSpaceAvailable(path_, &available_bytes)); |
| if (used_bytes + available_bytes < capacity_) { |
| const string& err = Substitute("Insufficient space for $0. Total required $1 and " |
| "cache already used $2. Still required $3, but only $4 is available", |
| path_, PrettyPrinter::PrintBytes(capacity_), PrettyPrinter::PrintBytes(used_bytes), |
| PrettyPrinter::PrintBytes(capacity_ - used_bytes), |
| PrettyPrinter::PrintBytes(available_bytes)); |
| LOG(ERROR) << err; |
| return Status(err); |
| } |
| |
| return Status::OK(); |
| } |
| |
| void DataCache::Partition::InitMetrics() { |
| const string& i_string = Substitute("$0", index_); |
| // Backend tests may instantiate the data cache (and its associated partitions) |
| // more than once. If the metrics already exist, then we just need to look up the |
| // metrics to populate the partition's fields. |
| if (TestInfo::is_test() && |
| ImpaladMetrics::IO_MGR_METRICS->FindMetricForTesting<StringProperty>( |
| Substitute(PARTITION_PATH_METRIC_KEY_TEMPLATE, i_string)) != nullptr) { |
| // If the partition path metric already initialized, then all the other metrics |
| // must be initialized. |
| read_latency_ = |
| ImpaladMetrics::IO_MGR_METRICS->FindMetricForTesting<HistogramMetric>( |
| Substitute(PARTITION_READ_LATENCY_METRIC_KEY_TEMPLATE, i_string)); |
| write_latency_ = |
| ImpaladMetrics::IO_MGR_METRICS->FindMetricForTesting<HistogramMetric>( |
| Substitute(PARTITION_WRITE_LATENCY_METRIC_KEY_TEMPLATE, i_string)); |
| eviction_latency_ = |
| ImpaladMetrics::IO_MGR_METRICS->FindMetricForTesting<HistogramMetric>( |
| Substitute(PARTITION_EVICTION_LATENCY_METRIC_KEY_TEMPLATE, i_string)); |
| DCHECK(read_latency_ != nullptr); |
| DCHECK(write_latency_ != nullptr); |
| DCHECK(eviction_latency_ != nullptr); |
| return; |
| } |
| // Two cases: |
| // - This is not a backend test, so metrics should only be initialized once. |
| // - This is a backend test, but none of the metrics have been initialized before. |
| DCHECK(read_latency_ == nullptr); |
| DCHECK(write_latency_ == nullptr); |
| DCHECK(eviction_latency_ == nullptr); |
| int64_t ONE_HOUR_IN_NS = 60L * 60L * NANOS_PER_SEC; |
| ImpaladMetrics::IO_MGR_METRICS->AddProperty<string>( |
| PARTITION_PATH_METRIC_KEY_TEMPLATE, path_, i_string); |
| read_latency_ = ImpaladMetrics::IO_MGR_METRICS->RegisterMetric(new HistogramMetric( |
| MetricDefs::Get(PARTITION_READ_LATENCY_METRIC_KEY_TEMPLATE, i_string), |
| ONE_HOUR_IN_NS, 3)); |
| write_latency_ = ImpaladMetrics::IO_MGR_METRICS->RegisterMetric(new HistogramMetric( |
| MetricDefs::Get(PARTITION_WRITE_LATENCY_METRIC_KEY_TEMPLATE, i_string), |
| ONE_HOUR_IN_NS, 3)); |
| eviction_latency_ = |
| ImpaladMetrics::IO_MGR_METRICS->RegisterMetric(new HistogramMetric( |
| MetricDefs::Get(PARTITION_EVICTION_LATENCY_METRIC_KEY_TEMPLATE, i_string), |
| ONE_HOUR_IN_NS, 3)); |
| } |
| |
| Status DataCache::Partition::CloseFilesAndVerifySizes() { |
| if (trace_replay_) return Status::OK(); |
| int64_t total_size = 0; |
| for (auto& file : cache_files_) { |
| uint64_t sz_on_disk; |
| // Close the backing files before checking sizes as some filesystems (e.g. XFS) |
| // preallocate the file beyond EOF. Closing the file removes any preallocation. |
| file->Close(); |
| kudu::Env* env = kudu::Env::Default(); |
| KUDU_RETURN_IF_ERROR(env->GetFileSizeOnDisk(file->path(), &sz_on_disk), |
| "CloseFilesAndVerifySizes()"); |
| total_size += sz_on_disk; |
| uint64_t logical_sz; |
| KUDU_RETURN_IF_ERROR(env->GetFileSize(file->path(), &logical_sz), |
| "CloseFilesAndVerifySizes()"); |
| DCHECK_LE(logical_sz, FLAGS_data_cache_file_max_size_bytes); |
| } |
| if (total_size > capacity_) { |
| return Status(Substitute("Partition $0 consumed $1 bytes, exceeding capacity of $2 " |
| "bytes", path_, total_size, capacity_)); |
| } |
| return Status::OK(); |
| } |
| |
| void DataCache::Partition::ReleaseResources() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| if (closed_) return; |
| closed_ = true; |
| // Close and delete all backing files in this partition. |
| cache_files_.clear(); |
| // Free all memory consumed by the metadata cache. |
| meta_cache_.reset(); |
| } |
| |
| int64_t DataCache::Partition::Lookup(const CacheKey& cache_key, int64_t bytes_to_read, |
| uint8_t* buffer) { |
| DCHECK(!closed_); |
| DCHECK(trace_replay_ ? buffer == nullptr : buffer != nullptr); |
| Slice key = cache_key.ToSlice(); |
| Cache::UniqueHandle handle(meta_cache_->Lookup(key)); |
| |
| if (handle.get() == nullptr) { |
| Trace(trace::EventType::MISS, cache_key, bytes_to_read, /*entry_len=*/-1); |
| return 0; |
| } |
| |
| // Read from the backing file. |
| CacheEntry entry(meta_cache_->Value(handle)); |
| |
| Trace(trace::EventType::HIT, cache_key, bytes_to_read, entry.len()); |
| |
| bytes_to_read = min(entry.len(), bytes_to_read); |
| // Skip the actual reads if doing trace replay |
| if (LIKELY(!trace_replay_)) { |
| CacheFile* cache_file = entry.file(); |
| VLOG(3) << Substitute("Reading file $0 offset $1 len $2 checksum $3 bytes_to_read $4", |
| cache_file->path(), entry.offset(), entry.len(), entry.checksum(), bytes_to_read); |
| bool read_success; |
| { |
| ScopedHistogramTimer read_timer(read_latency_); |
| read_success = cache_file->Read(entry.offset(), buffer, bytes_to_read); |
| } |
| if (UNLIKELY(!read_success)) { |
| meta_cache_->Erase(key); |
| return 0; |
| } |
| |
| // Verify checksum if enabled. Delete entry on checksum mismatch. |
| if (FLAGS_data_cache_checksum && bytes_to_read == entry.len() && |
| !VerifyChecksum("read", entry, buffer, bytes_to_read)) { |
| meta_cache_->Erase(key); |
| return 0; |
| } |
| } |
| return bytes_to_read; |
| } |
| |
| bool DataCache::Partition::HandleExistingEntry(const Slice& key, |
| const Cache::UniqueHandle& handle, const uint8_t* buffer, int64_t buffer_len) { |
| // Unpack the cache entry. |
| CacheEntry entry(meta_cache_->Value(handle)); |
| |
| // Trace replays have no data and cannot do checksums. |
| if (LIKELY(!trace_replay_)) { |
| // Try verifying the checksum of the new buffer matches that of the existing entry. |
| // On checksum mismatch, delete the existing entry and don't install the new entry |
| // as it's unclear which one is right. |
| if (FLAGS_data_cache_checksum && buffer_len >= entry.len()) { |
| if (!VerifyChecksum("write", entry, buffer, buffer_len)) { |
| meta_cache_->Erase(key); |
| return true; |
| } |
| } |
| } |
| // If the new entry is not any longer than the existing entry, no work to do. |
| return entry.len() >= buffer_len; |
| } |
| |
| bool DataCache::Partition::InsertIntoCache(const Slice& key, CacheFile* cache_file, |
| int64_t insertion_offset, const uint8_t* buffer, int64_t buffer_len) { |
| if (UNLIKELY(trace_replay_)) { |
| DCHECK(buffer == nullptr); |
| DCHECK(cache_file == nullptr); |
| } |
| DCHECK_EQ(insertion_offset % PAGE_SIZE, 0); |
| const int64_t charge_len = BitUtil::RoundUp(buffer_len, PAGE_SIZE); |
| |
| // Allocate a cache handle |
| Cache::UniquePendingHandle pending_handle( |
| meta_cache_->Allocate(key, sizeof(CacheEntry), charge_len)); |
| if (UNLIKELY(pending_handle.get() == nullptr)) return false; |
| |
| int64_t checksum = 0; |
| // Trace replays have no data and cannot do checksums |
| if (LIKELY(!trace_replay_)) { |
| // Compute checksum if necessary. |
| checksum = FLAGS_data_cache_checksum ? Checksum(buffer, buffer_len) : 0; |
| |
| // Write to backing file. |
| VLOG(3) << Substitute("Storing file $0 offset $1 len $2 checksum $3 ", |
| cache_file->path(), insertion_offset, buffer_len, checksum); |
| bool write_success; |
| { |
| ScopedHistogramTimer write_timer(write_latency_); |
| write_success = cache_file->Write(insertion_offset, buffer, buffer_len); |
| } |
| if (UNLIKELY(!write_success)) { |
| return false; |
| } |
| } |
| |
| // IMPALA-10971: These metrics need to be incremented prior to the Insert(), because |
| // the Insert() can fail and instantly evict the entry. When that happens, |
| // the total bytes and num entries are decremented. Without this corresponding |
| // increment, the counts will be incorrect. |
| // Trace replays do not keep metrics |
| if (LIKELY(!trace_replay_)) { |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_TOTAL_BYTES->Increment(charge_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_ENTRIES->Increment(1); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_WRITES->Increment(1); |
| } |
| |
| // Insert the new entry into the cache. |
| CacheEntry entry(cache_file, insertion_offset, buffer_len, checksum); |
| memcpy(meta_cache_->MutableValue(&pending_handle), &entry, sizeof(CacheEntry)); |
| Cache::UniqueHandle handle(meta_cache_->Insert(std::move(pending_handle), this)); |
| // Check for failure of Insert(), which means the entry was evicted during Insert() |
| if (UNLIKELY(handle.get() == nullptr)){ |
| // Trace replays do not keep metrics |
| if (LIKELY(!trace_replay_)) { |
| // EvictedEntry() already ran and decremented the other counters. |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_INSTANT_EVICTIONS->Increment(1); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| bool DataCache::Partition::Store(const CacheKey& cache_key, const uint8_t* buffer, |
| int64_t buffer_len, bool* start_reclaim) { |
| DCHECK(!closed_); |
| *start_reclaim = false; |
| Slice key = cache_key.ToSlice(); |
| const int64_t charge_len = BitUtil::RoundUp(buffer_len, PAGE_SIZE); |
| if (charge_len > capacity_) return false; |
| |
| // Check for existing entry. |
| { |
| Cache::UniqueHandle handle(meta_cache_->Lookup(key, Cache::NO_UPDATE)); |
| if (handle.get() != nullptr) { |
| if (HandleExistingEntry(key, handle, buffer, buffer_len)) return false; |
| } |
| } |
| |
| CacheFile* cache_file; |
| int64_t insertion_offset; |
| if (LIKELY(!trace_replay_)) { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| |
| // Limit the write concurrency to avoid blocking the caller (which could be calling |
| // from the critical path of an IO read) when the cache becomes IO bound due to either |
| // limited memory for page cache or the cache is undersized which leads to eviction. |
| const bool exceed_concurrency = |
| pending_insert_set_.size() >= data_cache_write_concurrency_; |
| if (exceed_concurrency || |
| pending_insert_set_.find(key.ToString()) != pending_insert_set_.end()) { |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_DROPPED_BYTES->Increment(buffer_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_DROPPED_ENTRIES->Increment(1); |
| Trace(trace::EventType::STORE_FAILED_BUSY, cache_key, /*lookup_len=*/-1, |
| buffer_len); |
| return false; |
| } |
| |
| // Allocate from the backing file. |
| CHECK(!cache_files_.empty()); |
| cache_file = cache_files_.back().get(); |
| insertion_offset = cache_file->Allocate(charge_len, partition_lock); |
| // Create and append to a new file if necessary. |
| if (UNLIKELY(insertion_offset < 0)) { |
| if (!CreateCacheFile().ok()) return false; |
| cache_file = cache_files_.back().get(); |
| insertion_offset = cache_file->Allocate(charge_len, partition_lock); |
| if (UNLIKELY(insertion_offset < 0)) return false; |
| } |
| |
| // Start deleting old files if there are too many opened. |
| *start_reclaim = cache_files_.size() > max_opened_files_; |
| |
| // Do this last. At this point, we are committed to inserting 'key' into the cache. |
| pending_insert_set_.emplace(key.ToString()); |
| } else { |
| DCHECK(buffer == nullptr); |
| cache_file = nullptr; |
| insertion_offset = 0; |
| } |
| |
| // Set up a scoped exit to always remove entry from the pending insertion set. |
| // This is not needed for trace replay. |
| auto remove_from_pending_set = MakeScopeExitTrigger([this, &key]() { |
| if (UNLIKELY(trace_replay_)) return; |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| pending_insert_set_.erase(key.ToString()); |
| }); |
| |
| // Try inserting into the cache. |
| bool insert_success = InsertIntoCache(key, cache_file, insertion_offset, buffer, |
| buffer_len); |
| if (insert_success) { |
| Trace(trace::EventType::STORE, cache_key, /* lookup_len=*/-1, buffer_len); |
| } else { |
| Trace(trace::EventType::STORE_FAILED, cache_key, /*lookup_len=*/ -1, buffer_len); |
| } |
| return insert_success; |
| } |
| |
| void DataCache::Partition::DeleteOldFiles() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| if (UNLIKELY(files_readonly_)) return; |
| DCHECK_GE(oldest_opened_file_, 0); |
| int target = cache_files_.size() - FLAGS_data_cache_max_opened_files; |
| while (oldest_opened_file_ < target) { |
| cache_files_[oldest_opened_file_++]->DeleteFile(); |
| } |
| } |
| |
| void DataCache::Partition::SetCacheFilesReadOnly() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| for (auto& file : cache_files_) file->SetReadOnly(); |
| files_readonly_ = true; |
| } |
| |
| void DataCache::Partition::RevokeCacheFilesReadOnly() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| for (auto& file : cache_files_) file->RevokeReadOnly(); |
| files_readonly_ = false; |
| } |
| |
| Status DataCache::Partition::Dump() { |
| std::unique_lock<SpinLock> partition_lock(lock_); |
| LOG(INFO) << Substitute("Partition $0 start dumping.", index_); |
| DumpData dump_data; |
| RETURN_IF_ERROR(DumpCacheFiles(dump_data)); |
| RETURN_IF_ERROR(DumpMetaCache(dump_data)); |
| std::ofstream file; |
| file.open(JoinPathSegments(path_, DUMP_FILE_NAME), std::ofstream::binary); |
| if (!file) return Status(Substitute("Failed to open $0", DUMP_FILE_NAME)); |
| try { |
| dump_data.serialize(file); |
| } catch (boost::archive::archive_exception& e) { |
| LOG(ERROR) << Substitute("Partition $0 failed to serialize dump file.", index_) |
| << e.what(); |
| return Status("Failed to serialize dump file."); |
| } |
| file.close(); |
| LOG(INFO) << Substitute("Partition $0 dump successfully.", index_); |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::Load() { |
| std::ifstream file; |
| file.open(JoinPathSegments(path_, DUMP_FILE_NAME), std::ofstream::binary); |
| if (!file) return Status::Expected(Substitute("Failed to open $0", DUMP_FILE_NAME)); |
| LOG(INFO) << Substitute("Partition $0 start loading.", index_); |
| DumpData dump_data; |
| try { |
| dump_data.deserialize(file); |
| } catch (boost::archive::archive_exception& e) { |
| LOG(ERROR) << Substitute("Partition $0 failed to deserialize dump file.", index_) |
| << e.what(); |
| return Status("Failed to deserialize dump file."); |
| } |
| file.close(); |
| |
| Status status = LoadCacheFiles(dump_data); |
| if (!status.ok()) { |
| cache_files_.clear(); |
| return status; |
| } |
| |
| status = LoadMetaCache(dump_data); |
| if (!status.ok()) { |
| cache_files_.clear(); |
| meta_cache_.reset(NewCache(GetCacheEvictionPolicy(FLAGS_data_cache_eviction_policy), |
| capacity_, path_)); |
| // We have already successfully initialized meta_cache_ once before calling Load(), so |
| // there is no reason for it to fail this time. |
| ABORT_IF_ERROR(meta_cache_->Init()); |
| return status; |
| } |
| |
| LOG(INFO) << Substitute("Partition $0 load successfully.", index_); |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::DumpCacheFiles(DumpData& dump_data) { |
| lock_.DCheckLocked(); |
| int64_t index = 0; |
| for (const auto& file : cache_files_) { |
| // There is no need to dump closed file, it can no longer be read or written or even |
| // deleted. |
| if (file->closed()) continue; |
| |
| // Just to be safe, flush the file. |
| RETURN_IF_ERROR(file->Flush()); |
| |
| dump_data.cache_files_data.emplace_back(file->path(), file->allow_append(), |
| file->current_offset(), file->mtime()); |
| auto result = dump_data.file_indexs.emplace(file.get(), index++); |
| DCHECK(result.second); |
| } |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::LoadCacheFiles(const DumpData& dump_data) { |
| lock_.DCheckLocked(); |
| cache_files_.clear(); |
| for (const DumpData::CacheFileData& file_data : dump_data.cache_files_data) { |
| unique_ptr<CacheFile> cache_file; |
| RETURN_IF_ERROR(CacheFile::Open(file_data.path, file_data.allow_append, |
| file_data.current_offset, &cache_file)); |
| if (file_data.mtime != cache_file->mtime()) { |
| return Status("The actual mtime of the cache file does not match the dumped data."); |
| } |
| cache_files_.emplace_back(std::move(cache_file)); |
| } |
| if (cache_files_.empty()) { |
| return Status("No cache files loaded."); |
| } |
| oldest_opened_file_ = 0; |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::DumpMetaCache(DumpData& dump_data) { |
| lock_.DCheckLocked(); |
| DCHECK(dump_data.cache_files_data.size() == dump_data.file_indexs.size()); |
| |
| // Walk through the meta cache, taking all keys and entries and dumping them one by one. |
| vector<Cache::UniqueHandle> handles = meta_cache_->Dump(); |
| for (const auto& handle : handles) { |
| CacheEntry entry(meta_cache_->Value(handle)); |
| auto index_iter = dump_data.file_indexs.find(entry.file()); |
| // If the entry's cache file is not in the list, we do not need to dump it. |
| if (index_iter == dump_data.file_indexs.end()) continue; |
| dump_data.meta_cache_data.emplace_back(meta_cache_->Key(handle).ToString(), |
| index_iter->second, entry.offset(), entry.len(), entry.checksum()); |
| } |
| return Status::OK(); |
| } |
| |
| Status DataCache::Partition::LoadMetaCache(const DumpData& dump_data) { |
| lock_.DCheckLocked(); |
| for (const DumpData::CacheKeyEntryData& key_entry : dump_data.meta_cache_data) { |
| // Usually this shouldn't happen, and if it did, the dump file may be corrupted and |
| // loading should be aborted. |
| if (UNLIKELY(key_entry.index >= cache_files_.size())) { |
| return Status("Failed to load meta cache, the dump file may be corrupted."); |
| } |
| |
| // According to the index, find the corresponding file pointer, build entry and insert |
| // into the cache like normal store. If the cache capacity has been changed, the entry |
| // may be evicted, but that's no matter. |
| CacheFile* file = cache_files_[key_entry.index].get(); |
| CacheEntry entry(file, key_entry.offset, key_entry.len, key_entry.checksum); |
| const int64_t charge_len = BitUtil::RoundUp(entry.len(), PAGE_SIZE); |
| Cache::UniquePendingHandle pending_handle( |
| meta_cache_->Allocate(Slice(key_entry.key), sizeof(CacheEntry), charge_len)); |
| if (UNLIKELY(pending_handle.get() == nullptr)) continue; |
| memcpy(meta_cache_->MutableValue(&pending_handle), &entry, sizeof(CacheEntry)); |
| meta_cache_->Insert(std::move(pending_handle), this); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_TOTAL_BYTES->Increment(charge_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_ENTRIES->Increment(1); |
| } |
| return Status::OK(); |
| } |
| |
| void DataCache::Partition::EvictedEntry(Slice key, Slice value) { |
| if (closed_) return; |
| if (UNLIKELY(trace_replay_)) return; |
| ScopedHistogramTimer eviction_timer(eviction_latency_); |
| // Unpack the cache entry. |
| CacheEntry entry(value); |
| int64_t eviction_len = BitUtil::RoundUp(entry.len(), PAGE_SIZE); |
| DCHECK_EQ(entry.offset() % PAGE_SIZE, 0); |
| entry.file()->PunchHole(entry.offset(), eviction_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_TOTAL_BYTES->Increment(-eviction_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_ENTRIES->Increment(-1); |
| } |
| |
| // TODO: Switch to using CRC32 once we fix the TODO in hash-util.h |
| uint64_t DataCache::Partition::Checksum(const uint8_t* buffer, int64_t buffer_len) { |
| return HashUtil::FastHash64(buffer, buffer_len, 0xcafebeef); |
| } |
| |
| bool DataCache::Partition::VerifyChecksum(const string& ops_name, const CacheEntry& entry, |
| const uint8_t* buffer, int64_t buffer_len) { |
| DCHECK(FLAGS_data_cache_checksum); |
| DCHECK_GE(buffer_len, entry.len()); |
| int64_t checksum = Checksum(buffer, entry.len()); |
| if (UNLIKELY(checksum != entry.checksum())) { |
| LOG(DFATAL) << Substitute("Checksum mismatch during $0 for file $1 " |
| "offset: $2 len: $3 buffer len: $4. Expected $5, Got $6.", ops_name, |
| entry.file()->path(), entry.offset(), entry.len(), buffer_len, entry.checksum(), |
| checksum); |
| return false; |
| } |
| return true; |
| } |
| |
| DataCache::DataCache( |
| std::string config, int32_t num_async_write_threads, bool trace_replay) |
| : config_(move(config)), |
| trace_replay_(trace_replay), |
| num_async_write_threads_(num_async_write_threads) {} |
| |
| DataCache::~DataCache() { ReleaseResources(); } |
| |
| Status DataCache::Init() { |
| // Verifies all the configured flags are sane. |
| if (FLAGS_data_cache_file_max_size_bytes < PAGE_SIZE) { |
| return Status(Substitute("Misconfigured --data_cache_file_max_size_bytes: $0 bytes. " |
| "Must be at least $1 bytes", FLAGS_data_cache_file_max_size_bytes, PAGE_SIZE)); |
| } |
| if (FLAGS_data_cache_write_concurrency < 0) { |
| return Status(Substitute("Misconfigured --data_cache_write_concurrency: $0. " |
| "Must be at least 0; 0 uses a device-specific default.", |
| FLAGS_data_cache_write_concurrency)); |
| } |
| |
| // The expected form of the configuration string is: dir1,dir2,..,dirN:capacity |
| // Example: /tmp/data1,/tmp/data2:1TB |
| vector<string> all_cache_configs = Split(config_, ":", SkipEmpty()); |
| if (all_cache_configs.size() != 2) { |
| return Status(Substitute("Malformed data cache configuration $0", config_)); |
| } |
| |
| // Parse the capacity string to make sure it's well-formed. |
| bool is_percent; |
| per_partition_capacity_ = ParseUtil::ParseMemSpec(all_cache_configs[1], &is_percent, 0); |
| if (is_percent) { |
| return Status(Substitute("Malformed data cache capacity configuration $0", |
| all_cache_configs[1])); |
| } |
| if (per_partition_capacity_ < PAGE_SIZE) { |
| return Status(Substitute("Configured data cache capacity $0 is too small", |
| all_cache_configs[1])); |
| } |
| |
| set<string> cache_dirs; |
| SplitStringToSetUsing(all_cache_configs[0], ",", &cache_dirs); |
| int max_opened_files_per_partition = |
| FLAGS_data_cache_max_opened_files / cache_dirs.size(); |
| if (max_opened_files_per_partition < 1) { |
| return Status(Substitute("Misconfigured --data_cache_max_opened_files: $0. Must be " |
| "at least $1.", FLAGS_data_cache_max_opened_files, cache_dirs.size())); |
| } |
| int32_t partition_idx = 0; |
| for (const string& dir_path : cache_dirs) { |
| LOG(INFO) << "Adding partition " << dir_path << " with capacity " |
| << PrettyPrinter::PrintBytes(per_partition_capacity_); |
| std::unique_ptr<Partition> partition = |
| make_unique<Partition>(partition_idx, dir_path, per_partition_capacity_, |
| max_opened_files_per_partition, trace_replay_); |
| RETURN_IF_ERROR(partition->Init()); |
| partitions_.emplace_back(move(partition)); |
| ++partition_idx; |
| } |
| CHECK_GT(partitions_.size(), 0); |
| |
| if (LIKELY(!trace_replay_)) { |
| // Starts a thread pool which deletes old files from partitions. DataCache::Store() |
| // will enqueue a request (i.e. a partition index) when it notices the number of files |
| // in a partition exceeds the per-partition limit. The files in a partition will be |
| // closed in the order they are created until it's within the per-partition limit. |
| file_deleter_pool_.reset(new ThreadPool<int>("data-cache", |
| "data-cache-file-deleter", 1, MAX_FILE_DELETER_QUEUE_SIZE, |
| bind<void>(&DataCache::DeleteOldFiles, this, _1, _2))); |
| RETURN_IF_ERROR(file_deleter_pool_->Init()); |
| } |
| |
| /// If --data_cache_num_async_write_threads has been set above 0, the store behavior |
| /// will be asynchronous. A task queue and thread pool will be started for that. |
| if (num_async_write_threads_ > 0) { |
| if (UNLIKELY(trace_replay_)) { |
| return Status("Data cache does not support asynchronous writes when doing trace " |
| "replay. Please set 'data_cache_num_async_write_threads' to 0."); |
| } |
| // Parse the buffer limit string to make sure it's well-formed. |
| bool is_percent; |
| int64_t buffer_limit = ParseUtil::ParseMemSpec( |
| FLAGS_data_cache_async_write_buffer_limit, &is_percent, 0); |
| if (is_percent) { |
| return Status(Substitute("Malformed data cache write buffer limit configuration $0", |
| FLAGS_data_cache_async_write_buffer_limit)); |
| } |
| if (!TestInfo::is_be_test() && buffer_limit < (1 << 23 /* 8MB */)) { |
| return Status(Substitute("Configured data cache write buffer limit $0 is too small " |
| "(less than 8MB)", FLAGS_data_cache_async_write_buffer_limit)); |
| } |
| store_buffer_capacity_ = buffer_limit; |
| storer_pool_.reset(new ThreadPool<StoreTaskHandle>("data-cache", "data-cache-storer", |
| num_async_write_threads_, MAX_STORE_TASK_QUEUE_SIZE, |
| bind<void>(&DataCache::HandleStoreTask, this, _1, _2))); |
| RETURN_IF_ERROR(storer_pool_->Init()); |
| } |
| |
| return Status::OK(); |
| } |
| |
| void DataCache::ReleaseResources() { |
| if (storer_pool_) { |
| storer_pool_->Shutdown(); |
| storer_pool_->Join(); |
| } |
| if (file_deleter_pool_) file_deleter_pool_->Shutdown(); |
| for (auto& partition : partitions_) partition->ReleaseResources(); |
| } |
| |
| int64_t DataCache::Lookup(const string& filename, int64_t mtime, int64_t offset, |
| int64_t bytes_to_read, uint8_t* buffer) { |
| DCHECK(!partitions_.empty()); |
| // Bail out early for uncacheable ranges or invalid requests. |
| if (mtime < 0 || offset < 0 || bytes_to_read < 0) { |
| VLOG(3) << Substitute("Skipping lookup of invalid entry $0 mtime: $1 offset: $2 " |
| "bytes_to_read: $3", filename, mtime, offset, bytes_to_read); |
| return 0; |
| } |
| |
| // Construct a cache key. The cache key is also hashed to compute the partition index. |
| const CacheKey key(filename, mtime, offset); |
| int idx = key.Hash() % partitions_.size(); |
| int64_t bytes_read = partitions_[idx]->Lookup(key, bytes_to_read, buffer); |
| if (VLOG_IS_ON(3)) { |
| stringstream ss; |
| ss << std::hex << reinterpret_cast<int64_t>(buffer); |
| LOG(INFO) << Substitute("Looking up $0 mtime: $1 offset: $2 bytes_to_read: $3 " |
| "buffer: 0x$4 bytes_read: $5", filename, mtime, offset, bytes_to_read, |
| ss.str(), bytes_read); |
| } |
| return bytes_read; |
| } |
| |
| bool DataCache::Store(const string& filename, int64_t mtime, int64_t offset, |
| const uint8_t* buffer, int64_t buffer_len) { |
| DCHECK(!partitions_.empty()); |
| |
| // Check early that the cache is read-only. |
| if (UNLIKELY(readonly_.Load())) return false; |
| |
| // Bail out early for uncacheable ranges or invalid requests. |
| if (mtime < 0 || offset < 0 || buffer_len < 0) { |
| VLOG(3) << Substitute("Skipping insertion of invalid entry $0 mtime: $1 offset: $2 " |
| "buffer_len: $3", filename, mtime, offset, buffer_len); |
| return false; |
| } |
| |
| // If the storer thread pool is available, data will be stored asynchronously. |
| if (num_async_write_threads_ > 0) { |
| return SubmitStoreTask(filename, mtime, offset, buffer, buffer_len); |
| } |
| |
| // Construct a cache key. The cache key is also hashed to compute the partition index. |
| const CacheKey key(filename, mtime, offset); |
| return StoreInternal(key, buffer, buffer_len); |
| } |
| |
| Status DataCache::CloseFilesAndVerifySizes() { |
| for (auto& partition : partitions_) { |
| RETURN_IF_ERROR(partition->CloseFilesAndVerifySizes()); |
| } |
| return Status::OK(); |
| } |
| |
| int64_t DataCache::SetDataCacheReadOnly() { |
| if (readonly_.Load()) { |
| return ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_WRITES->GetValue(); |
| } |
| |
| // First set the read-only flag to reject new writes. The exclusive lock is then |
| // acquired, because the shared lock will be acquired before any writes begin, so it |
| // blocks here until all ongoing writes have completed and all shared lock has been |
| // released. This ensures that there will be no more change to the cache after the |
| // function returns. |
| readonly_.Store(true); |
| std::unique_lock<shared_mutex> lock(readonly_lock_); |
| for (auto& partition : partitions_) { |
| partition->SetCacheFilesReadOnly(); |
| } |
| |
| return ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_WRITES->GetValue(); |
| } |
| |
| int64_t DataCache::RevokeDataCacheReadOnly() { |
| if (!readonly_.Load()) { |
| return ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_WRITES->GetValue(); |
| } |
| |
| std::unique_lock<shared_mutex> lock(readonly_lock_); |
| for (auto& partition : partitions_) { |
| partition->RevokeCacheFilesReadOnly(); |
| } |
| readonly_.Store(false); |
| |
| return ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_WRITES->GetValue(); |
| } |
| |
| Status DataCache::Dump() { |
| SetDataCacheReadOnly(); |
| for (auto& partition : partitions_) { |
| RETURN_IF_ERROR(partition->Dump()); |
| } |
| return Status::OK(); |
| } |
| |
| void DataCache::DeleteOldFiles(uint32_t thread_id, int partition_idx) { |
| DCHECK_LT(partition_idx, partitions_.size()); |
| partitions_[partition_idx]->DeleteOldFiles(); |
| } |
| |
| bool DataCache::SubmitStoreTask(const std::string& filename, int64_t mtime, |
| int64_t offset, const uint8_t* buffer, int64_t buffer_len) { |
| const int64_t charge_len = BitUtil::RoundUp(buffer_len, PAGE_SIZE); |
| if (UNLIKELY(charge_len > per_partition_capacity_)) return false; |
| |
| // Tries to increase the current_buffer_size_ by buffer_len before allocate buffer. |
| // If new size exceeds store_buffer_capacity_, return false and current_buffer_size_ is |
| // not changed. |
| while (true) { |
| int64_t current_size = current_buffer_size_.Load(); |
| int64_t new_size = current_size + buffer_len; |
| if (UNLIKELY(new_size > store_buffer_capacity_)) { |
| VLOG(2) << Substitute("Failed to create store task due to buffer size limitation, " |
| "current buffer size: $0 size limitation: $1 require: $2", |
| current_size, store_buffer_capacity_, buffer_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_ASYNC_WRITES_DROPPED_BYTES-> |
| Increment(buffer_len); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_ASYNC_WRITES_DROPPED_ENTRIES->Increment(1); |
| return false; |
| } |
| if (LIKELY(current_buffer_size_.CompareAndSwap(current_size, new_size))) { |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_ASYNC_WRITES_OUTSTANDING_BYTES->SetValue( |
| current_buffer_size_.Load()); |
| break; |
| } |
| } |
| |
| DCHECK(buffer != nullptr); |
| // TODO: Should we use buffer pool instead of piecemeal memory allocate? |
| uint8_t* task_buffer = new uint8_t[buffer_len]; |
| memcpy(task_buffer, buffer, buffer_len); |
| |
| const StoreTask* task = |
| new StoreTask(filename, mtime, offset, task_buffer, buffer_len, this); |
| storer_pool_->Offer(StoreTaskHandle(task)); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_NUM_ASYNC_WRITES_SUBMITTED->Increment(1); |
| return true; |
| } |
| |
| void DataCache::CompleteStoreTask(const StoreTask& task) { |
| current_buffer_size_.Add(-task.buffer_len()); |
| DCHECK_GE(current_buffer_size_.Load(), 0); |
| ImpaladMetrics::IO_MGR_REMOTE_DATA_CACHE_ASYNC_WRITES_OUTSTANDING_BYTES->SetValue( |
| current_buffer_size_.Load()); |
| } |
| |
| void DataCache::HandleStoreTask(uint32_t thread_id, const StoreTaskHandle& task) { |
| StoreInternal(task->key(), task->buffer(), task->buffer_len()); |
| } |
| |
| bool DataCache::StoreInternal(const CacheKey& key, const uint8_t* buffer, |
| int64_t buffer_len) { |
| // Here, a shared lock is acquired with try_to_lock, while a unique lock is acquired in |
| // SetDataCacheReadOnly(). Therefore, when setting the read-only status, the lock |
| // acquisition here can quickly fail, then return. |
| kudu::shared_lock<shared_mutex> lock(readonly_lock_, std::try_to_lock); |
| if (UNLIKELY(!lock.owns_lock() || readonly_.Load())) return false; |
| |
| int idx = key.Hash() % partitions_.size(); |
| bool start_reclaim; |
| bool stored = partitions_[idx]->Store(key, buffer, buffer_len, &start_reclaim); |
| if (VLOG_IS_ON(3)) { |
| stringstream ss; |
| ss << std::hex << reinterpret_cast<int64_t>(buffer); |
| LOG(INFO) << Substitute("Storing $0 mtime: $1 offset: $2 bytes_to_read: $3 " |
| "buffer: 0x$4 stored: $5", key.filename().ToString(), key.mtime(), key.offset(), |
| buffer_len, ss.str(), stored); |
| } |
| if (start_reclaim) file_deleter_pool_->Offer(idx); |
| return stored; |
| } |
| |
| void DataCache::Partition::Trace( |
| const trace::EventType& type, const DataCache::CacheKey& key, |
| int64_t lookup_len, int64_t entry_len) { |
| if (tracer_ == nullptr) return; |
| |
| // When tracing a percentage of the requests, we want to trace all the accesses for a |
| // consistent subset of the entries rather than a subset of accesses for all entries. |
| // This gives the access trace more useful data. |
| // |
| // This uses the hash to determine a consistent subset to trace. Note that this is |
| // tracing at the partition level. If there are multiple partitions, the entry has |
| // been mapped to a specific partition by taking a modulus of the hash value. This |
| // can impact which bits are still useful. For example, if there are two partitions, |
| // this would have only even hash values or only odd hash values. To minimize the |
| // impact of this, we use 101 rather than 100, because 101 is prime. |
| uint64_t unsigned_key_hash = static_cast<uint64_t>(key.Hash()); |
| if (FLAGS_data_cache_trace_percentage < 100 && |
| unsigned_key_hash % 101 >= FLAGS_data_cache_trace_percentage) { |
| return; |
| } |
| |
| tracer_->Trace(type, WallTime_Now(), key.filename(), key.mtime(), key.offset(), |
| lookup_len, entry_len); |
| } |
| |
| } // namespace io |
| } // namespace impala |