blob: eb47b0839b8049a52b7e7c35f9e1c1ac83d10e99 [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.
#include "kudu/fs/log_block_manager.h"
#include <errno.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <numeric>
#include <ostream>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <boost/optional/optional.hpp>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include "kudu/fs/block_manager_metrics.h"
#include "kudu/fs/data_dirs.h"
#include "kudu/fs/dir_manager.h"
#include "kudu/fs/dir_util.h"
#include "kudu/fs/error_manager.h"
#include "kudu/fs/fs.pb.h"
#include "kudu/fs/fs_report.h"
#include "kudu/gutil/casts.h"
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/port.h"
#include "kudu/gutil/strings/numbers.h"
#include "kudu/gutil/strings/strcat.h"
#include "kudu/gutil/strings/strip.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/gutil/strings/util.h"
#include "kudu/gutil/walltime.h"
#include "kudu/util/alignment.h"
#include "kudu/util/array_view.h"
#include "kudu/util/env.h"
#include "kudu/util/file_cache.h"
#include "kudu/util/flag_tags.h"
#include "kudu/util/locks.h"
#include "kudu/util/malloc.h"
#include "kudu/util/metrics.h"
#include "kudu/util/path_util.h"
#include "kudu/util/pb_util.h"
#include "kudu/util/random.h"
#include "kudu/util/random_util.h"
#include "kudu/util/scoped_cleanup.h"
#include "kudu/util/slice.h"
#include "kudu/util/sorted_disjoint_interval_list.h"
#include "kudu/util/test_util_prod.h"
#include "kudu/util/trace.h"
DECLARE_bool(enable_data_block_fsync);
DECLARE_string(block_manager_preflush_control);
// TODO(unknown): How should this be configured? Should provide some guidance.
DEFINE_uint64(log_container_max_size, 10LU * 1024 * 1024 * 1024,
"Maximum size (soft) of a log container");
TAG_FLAG(log_container_max_size, advanced);
DEFINE_int64(log_container_max_blocks, -1,
"Maximum number of blocks (soft) of a log container. Use 0 for "
"no limit. Use -1 for no limit except in the case of a kernel "
"bug with hole punching on ext4 (see KUDU-1508 for details).");
TAG_FLAG(log_container_max_blocks, advanced);
DEFINE_uint64(log_container_preallocate_bytes, 32LU * 1024 * 1024,
"Number of bytes to preallocate in a log container when "
"creating new blocks. Set to 0 to disable preallocation");
TAG_FLAG(log_container_preallocate_bytes, advanced);
DEFINE_double(log_container_excess_space_before_cleanup_fraction, 0.10,
"Additional fraction of a log container's calculated size that "
"must be consumed on disk before the container is considered to "
"be inconsistent and subject to excess space cleanup at block "
"manager startup.");
TAG_FLAG(log_container_excess_space_before_cleanup_fraction, advanced);
DEFINE_double(log_container_live_metadata_before_compact_ratio, 0.50,
"Desired ratio of live block metadata in log containers. If a "
"container's live to total block ratio dips below this value, "
"the container's metadata file will be compacted at startup.");
TAG_FLAG(log_container_live_metadata_before_compact_ratio, experimental);
DEFINE_bool(log_block_manager_test_hole_punching, true,
"Ensure hole punching is supported by the underlying filesystem");
TAG_FLAG(log_block_manager_test_hole_punching, advanced);
TAG_FLAG(log_block_manager_test_hole_punching, unsafe);
DEFINE_bool(log_block_manager_delete_dead_container, true,
"When enabled, full and dead log block containers will be deleted "
"at runtime, which can potentially help improving log block manager "
"startup time");
TAG_FLAG(log_block_manager_delete_dead_container, advanced);
TAG_FLAG(log_block_manager_delete_dead_container, experimental);
METRIC_DEFINE_gauge_uint64(server, log_block_manager_bytes_under_management,
"Bytes Under Management",
kudu::MetricUnit::kBytes,
"Number of bytes of data blocks currently under management",
kudu::MetricLevel::kInfo);
METRIC_DEFINE_gauge_uint64(server, log_block_manager_blocks_under_management,
"Blocks Under Management",
kudu::MetricUnit::kBlocks,
"Number of data blocks currently under management",
kudu::MetricLevel::kInfo);
METRIC_DEFINE_gauge_uint64(server, log_block_manager_containers,
"Number of Block Containers",
kudu::MetricUnit::kLogBlockContainers,
"Number of log block containers",
kudu::MetricLevel::kInfo);
METRIC_DEFINE_gauge_uint64(server, log_block_manager_full_containers,
"Number of Full Block Containers",
kudu::MetricUnit::kLogBlockContainers,
"Number of full log block containers",
kudu::MetricLevel::kInfo);
METRIC_DEFINE_counter(server, log_block_manager_holes_punched,
"Number of Holes Punched",
kudu::MetricUnit::kHoles,
"Number of holes punched since service start",
kudu::MetricLevel::kDebug);
METRIC_DEFINE_counter(server, log_block_manager_dead_containers_deleted,
"Number of Dead Block Containers Deleted",
kudu::MetricUnit::kLogBlockContainers,
"Number of full (but dead) block containers that were deleted",
kudu::MetricLevel::kDebug);
namespace kudu {
namespace fs {
using internal::LogBlock;
using internal::LogBlockContainer;
using internal::LogBlockDeletionTransaction;
using internal::LogWritableBlock;
using pb_util::ReadablePBContainerFile;
using pb_util::WritablePBContainerFile;
using std::accumulate;
using std::map;
using std::set;
using std::shared_ptr;
using std::string;
using std::unique_ptr;
using std::unordered_map;
using std::unordered_set;
using std::vector;
using strings::Substitute;
namespace internal {
////////////////////////////////////////////////////////////
// LogBlockManagerMetrics
////////////////////////////////////////////////////////////
// Metrics container associated with the log block manager.
//
// Includes implementation-agnostic metrics as well as some that are
// specific to the log block manager.
struct LogBlockManagerMetrics {
explicit LogBlockManagerMetrics(const scoped_refptr<MetricEntity>& metric_entity);
// Implementation-agnostic metrics.
BlockManagerMetrics generic_metrics;
scoped_refptr<AtomicGauge<uint64_t>> bytes_under_management;
scoped_refptr<AtomicGauge<uint64_t>> blocks_under_management;
scoped_refptr<AtomicGauge<uint64_t>> containers;
scoped_refptr<AtomicGauge<uint64_t>> full_containers;
scoped_refptr<Counter> holes_punched;
scoped_refptr<Counter> dead_containers_deleted;
};
#define MINIT(x) x(METRIC_log_block_manager_##x.Instantiate(metric_entity))
#define GINIT(x) x(METRIC_log_block_manager_##x.Instantiate(metric_entity, 0))
LogBlockManagerMetrics::LogBlockManagerMetrics(const scoped_refptr<MetricEntity>& metric_entity)
: generic_metrics(metric_entity),
GINIT(bytes_under_management),
GINIT(blocks_under_management),
GINIT(containers),
GINIT(full_containers),
MINIT(holes_punched),
MINIT(dead_containers_deleted) {
}
#undef GINIT
////////////////////////////////////////////////////////////
// LogBlock (declaration)
////////////////////////////////////////////////////////////
// The persistent metadata that describes a logical block.
//
// A block grows a LogBlock when its data has been synchronized with
// the disk. That's when it's fully immutable (i.e. none of its metadata
// can change), and when it becomes readable and persistent.
//
// LogBlocks are reference counted to simplify support for deletion with
// outstanding readers. All refcount increments are performed with the
// block manager lock held, as are deletion-based decrements. However,
// no lock is held when ~LogReadableBlock decrements the refcount, thus it
// must be made thread safe (by extending RefCountedThreadSafe instead of
// the simpler RefCounted).
class LogBlock : public RefCountedThreadSafe<LogBlock> {
public:
LogBlock(LogBlockContainerRefPtr container, BlockId block_id, int64_t offset,
int64_t length);
~LogBlock() = default;
const BlockId& block_id() const { return block_id_; }
LogBlockContainer* container() const { return container_.get(); }
int64_t offset() const { return offset_; }
int64_t length() const { return length_; }
// Returns a block's length aligned to the nearest filesystem block size.
int64_t fs_aligned_length() const;
// Registers the deletion of the block with a deletion transaction. Actual
// deletion will take place when the deletion transaction is destructed.
void RegisterDeletion(const shared_ptr<LogBlockDeletionTransaction>& transaction);
private:
// The owning container.
LogBlockContainerRefPtr container_;
// The block identifier.
const BlockId block_id_;
// The block's offset in the container.
const int64_t offset_;
// The block's length.
const int64_t length_;
// The block deletion transaction with which this block has been registered.
shared_ptr<LogBlockDeletionTransaction> transaction_;
DISALLOW_COPY_AND_ASSIGN(LogBlock);
};
////////////////////////////////////////////////////////////
// LogWritableBlock (declaration)
////////////////////////////////////////////////////////////
// A log-backed block that has been opened for writing.
//
// There's no reference to a LogBlock as this block has yet to be
// persisted.
class LogWritableBlock : public WritableBlock {
public:
LogWritableBlock(LogBlockContainerRefPtr container, BlockId block_id,
int64_t block_offset);
virtual ~LogWritableBlock();
virtual Status Close() OVERRIDE;
virtual Status Abort() OVERRIDE;
virtual const BlockId& id() const OVERRIDE;
virtual BlockManager* block_manager() const OVERRIDE;
virtual Status Append(const Slice& data) OVERRIDE;
virtual Status AppendV(ArrayView<const Slice> data) OVERRIDE;
virtual Status Finalize() OVERRIDE;
virtual size_t BytesAppended() const OVERRIDE;
virtual State state() const OVERRIDE;
// Actually close the block, finalizing it if it has not yet been
// finalized. Also updates various metrics.
//
// This is called after synchronization of dirty data and metadata
// to disk.
void DoClose();
// Starts an asynchronous flush of dirty block data to disk.
Status FlushDataAsync();
// Write this block's metadata to disk.
//
// Does not synchronize the written data; that takes place in Close().
Status AppendMetadata();
LogBlockContainer* container() const { return container_.get(); }
private:
// The owning container.
LogBlockContainerRefPtr container_;
// The block's identifier.
const BlockId block_id_;
// The block's offset within the container. Known from the moment the
// block is created.
const int64_t block_offset_;
// The block's length. Changes with each Append().
int64_t block_length_;
// The state of the block describing where it is in the write lifecycle,
// for example, has it been synchronized to disk?
WritableBlock::State state_;
DISALLOW_COPY_AND_ASSIGN(LogWritableBlock);
};
////////////////////////////////////////////////////////////
// LogBlockContainer
////////////////////////////////////////////////////////////
// A single block container belonging to the log-backed block manager.
//
// A container may only be used to write one WritableBlock at a given time.
// However, existing blocks may be deleted concurrently. As such, almost
// all container functions must be reentrant, even if the container itself
// is logically thread unsafe (i.e. multiple clients calling WriteData()
// concurrently will produce nonsensical container data). Thread unsafe
// functions are marked explicitly.
//
// Block containers are reference counted so that they can be safely removed
// despite concurrent access.
class LogBlockContainer: public RefCountedThreadSafe<LogBlockContainer> {
public:
enum SyncMode {
SYNC,
NO_SYNC
};
static const char* kMagic;
// Creates a new block container in 'dir'.
static Status Create(LogBlockManager* block_manager,
Dir* dir,
LogBlockContainerRefPtr* container);
// Opens an existing block container in 'dir'.
//
// Every container is comprised of two files: "<dir>/<id>.data" and
// "<dir>/<id>.metadata". Together, 'dir' and 'id' fully describe both
// files.
//
// Returns Status::Aborted() in the case that the metadata and data files
// both appear to have no data (e.g. due to a crash just after creating
// one of them but before writing any records). This is recorded in 'report'.
static Status Open(LogBlockManager* block_manager,
Dir* dir,
FsReport* report,
const string& id,
LogBlockContainerRefPtr* container);
// The destructor will delete files of this container if it is dead.
~LogBlockContainer();
// Closes a set of blocks belonging to this container, possibly synchronizing
// the dirty data and metadata to disk.
//
// If successful, adds all blocks to the block manager's in-memory maps.
Status DoCloseBlocks(const vector<LogWritableBlock*>& blocks, SyncMode mode);
// Frees the space associated with a block or a group of blocks at 'offset'
// and 'length'. This is a physical operation, not a logical one; a separate
// AppendMetadata() is required to record the deletion in container metadata.
//
// The on-disk effects of this call are made durable only after SyncData().
Status PunchHole(int64_t offset, int64_t length);
// Executes a hole punching operation at 'offset' with the given 'length'.
void ContainerDeletionAsync(int64_t offset, int64_t length);
// Preallocate enough space to ensure that an append of 'next_append_length'
// can be satisfied by this container. The offset of the beginning of this
// block must be provided in 'block_start_offset' (since container
// bookkeeping is only updated when a block is finished).
//
// Does nothing if preallocation is disabled.
Status EnsurePreallocated(int64_t block_start_offset,
size_t next_append_length);
// See RWFile::Write()
Status WriteData(int64_t offset, const Slice& data);
// See RWFile::WriteV()
Status WriteVData(int64_t offset, ArrayView<const Slice> data);
// See RWFile::Read().
Status ReadData(int64_t offset, Slice result) const;
// See RWFile::ReadV().
Status ReadVData(int64_t offset, ArrayView<Slice> results) const;
// Appends 'pb' to this container's metadata file.
//
// The on-disk effects of this call are made durable only after SyncMetadata().
Status AppendMetadata(const BlockRecordPB& pb);
// Asynchronously flush this container's data file from 'offset' through
// to 'length'.
//
// Does not guarantee data durability; use SyncData() for that.
Status FlushData(int64_t offset, int64_t length);
// Asynchronously flush this container's metadata file (all dirty bits).
//
// Does not guarantee metadata durability; use SyncMetadata() for that.
//
// TODO(unknown): Add support to just flush a range.
Status FlushMetadata();
// Synchronize this container's data file with the disk. On success,
// guarantees that the data is made durable.
//
// TODO(unknown): Add support to synchronize just a range.
Status SyncData();
// Synchronize this container's metadata file with the disk. On success,
// guarantees that the metadata is made durable.
//
// TODO(unknown): Add support to synchronize just a range.
Status SyncMetadata();
// Reopen the metadata file record writer. Should be called if the underlying
// file was changed.
Status ReopenMetadataWriter();
// Truncates this container's data file to 'next_block_offset_' if it is
// full. This effectively removes any preallocated but unused space.
//
// Should be called only when 'next_block_offset_' is up-to-date with
// respect to the data on disk (i.e. after the container's records have
// been loaded), otherwise data may be lost!
//
// This function is thread unsafe.
Status TruncateDataToNextBlockOffset();
// Reads the container's metadata from disk, sanity checking and processing
// records along the way.
//
// Malformed records and other container inconsistencies are written to
// 'report'. Healthy blocks are written either to 'live_blocks' or
// 'dead_blocks'. Live records are written to 'live_block_records'. The
// greatest block ID seen thus far in the container is written to 'max_block_id'.
//
// Returns an error only if there was a problem accessing the container from
// disk; such errors are fatal and effectively halt processing immediately.
Status ProcessRecords(
FsReport* report,
LogBlockManager::UntrackedBlockMap* live_blocks,
LogBlockManager::BlockRecordMap* live_block_records,
vector<LogBlockRefPtr>* dead_blocks,
uint64_t* max_block_id);
// Updates internal bookkeeping state to reflect the creation of a block.
void BlockCreated(const LogBlockRefPtr& block);
// Updates internal bookkeeping state to reflect the deletion of a block.
//
// This function is thread safe because block deletions can happen concurrently
// with creations.
//
// Note: the container is not made "unfull"; containers remain sparse until deleted.
void BlockDeleted(const LogBlockRefPtr& block);
// Finalizes a fully written block. It updates the container data file's position,
// truncates the container if full and marks the container as available.
void FinalizeBlock(int64_t block_offset, int64_t block_length);
// Runs a task on this container's data directory thread pool.
//
// Normally the task is performed asynchronously. However, if submission to
// the pool fails, it runs synchronously on the current thread.
void ExecClosure(const std::function<void()>& task);
// Produces a debug-friendly string representation of this container.
string ToString() const;
// Makes the container read-only and stores the responsible error.
void SetReadOnly(const Status& error);
// Handles errors if the input status is not OK.
void HandleError(const Status& s) const;
// Returns whether or not the container has been marked read-only.
bool read_only() const {
return !read_only_status().ok();
}
// Returns the error that caused the container to become read-only, or OK if
// the container has not been marked read-only.
Status read_only_status() const {
std::lock_guard<simple_spinlock> l(read_only_lock_);
return read_only_status_;
}
// Simple accessors.
LogBlockManager* block_manager() const { return block_manager_; }
int64_t next_block_offset() const { return next_block_offset_.Load(); }
int64_t total_bytes() const { return total_bytes_.Load(); }
int64_t total_blocks() const { return total_blocks_.Load(); }
int64_t live_bytes() const { return live_bytes_.Load(); }
int64_t live_bytes_aligned() const { return live_bytes_aligned_.Load(); }
int64_t live_blocks() const { return live_blocks_.Load(); }
int32_t blocks_being_written() const { return blocks_being_written_.Load(); }
bool full() const {
return next_block_offset() >= FLAGS_log_container_max_size ||
(max_num_blocks_ && (total_blocks() >= max_num_blocks_));
}
bool dead() const { return dead_.Load(); }
const LogBlockManagerMetrics* metrics() const { return metrics_; }
Dir* data_dir() const { return data_dir_; }
const DirInstanceMetadataPB* instance() const { return data_dir_->instance()->metadata(); }
// Adjusts the number of blocks being written.
// Positive means increase, negative means decrease.
int32_t blocks_being_written_incr(int32_t value) {
return blocks_being_written_.IncrementBy(value);
}
// Check that the container meets the death condition.
//
// Although this code looks like a TOCTOU violation, it is safe because of
// some additional LBM invariants:
// 1) When a container becomes full, it stays full for the process' lifetime.
// 2) A full container will never accrue another live block. Meaning, losing
// its last live block is a terminal state for a full container.
// 3) The only exception to #2 is if the container currently has a finalized
// but not-yet-closed WritableBlock. In this case the container became full
// when the WritableBlock was finalized, but the live block counter only
// reflects the new block when it is closed.
bool check_death_condition() const {
return (full() && live_blocks() == 0 && blocks_being_written() == 0 &&
FLAGS_log_block_manager_delete_dead_container);
}
// Tries to mark the container as 'dead', which means it will be deleted
// when it goes out of scope. Can only be set dead once.
//
// If successful, returns true; otherwise returns false.
bool TrySetDead() {
if (dead()) return false;
return dead_.CompareAndSet(false, true);
}
private:
LogBlockContainer(LogBlockManager* block_manager, Dir* data_dir,
unique_ptr<WritablePBContainerFile> metadata_file,
shared_ptr<RWFile> data_file);
// Check the container whether it is fine.
//
// OK: both files of the container exist;
// Aborted: the container will be repaired later;
// NotFound: one file of the container has gone missing;
// *-------------*------------*----------------*-------------------------*---------------------*
// |DATA\METADATA| NONE EXIST | EXIST && < MIN | EXIST && NO LIVE BLOCKS | EXIST && LIVE BLOCKS|
// *-------------*------------*----------------*-------------------------*---------------------*
// | NONE EXIST | | Aborted | Aborted | NotFound |
// *-------------*------------*----------------*-------------------------*---------------------*
// | EXIST && 0 | Aborted | Aborted | OK | OK |
// *-------------*------------*----------------*-------------------------*---------------------*
// | EXIST && >0 | NotFound | OK | OK | OK |
// *-------------*------------*----------------*-------------------------*---------------------*
//
// Note: the status here only represents the result of check.
static Status CheckContainerFiles(LogBlockManager* block_manager,
FsReport* report,
const Dir* dir,
const string& common_path,
const string& data_path,
const string& metadata_path);
// Processes a single block record, performing sanity checks on it and adding
// it either to 'live_blocks' or 'dead_blocks'. If the record is live, it is
// added to 'live_block_records'.
//
// Returns an error only if there was a problem accessing the container from
// disk; such errors are fatal and effectively halt processing immediately.
//
// On success, 'report' is updated with any inconsistencies found in the
// record, 'data_file_size' may be updated with the latest size of the
// container's data file, and 'max_block_id' reflects the largest block ID
// seen thus far in the container.
//
// Note: 'record' may be swapped into 'report'; do not use it after calling
// this function.
Status ProcessRecord(
BlockRecordPB* record,
FsReport* report,
LogBlockManager::UntrackedBlockMap* live_blocks,
LogBlockManager::BlockRecordMap* live_block_records,
vector<LogBlockRefPtr>* dead_blocks,
uint64_t* data_file_size,
uint64_t* max_block_id);
// Updates this container data file's position based on the offset and length
// of a block, marking this container as full if needed. Should only be called
// when a block is fully written, as it will round up the container data file's
// position.
//
// This function is thread unsafe.
void UpdateNextBlockOffset(int64_t block_offset, int64_t block_length);
// The owning block manager. Must outlive the container itself.
LogBlockManager* const block_manager_;
// The data directory where the container lives.
Dir* data_dir_;
const boost::optional<int64_t> max_num_blocks_;
// Offset up to which we have preallocated bytes.
int64_t preallocated_offset_ = 0;
// Opened file handles to the container's files.
unique_ptr<WritablePBContainerFile> metadata_file_;
shared_ptr<RWFile> data_file_;
// The offset of the next block to be written to the container.
AtomicInt<int64_t> next_block_offset_;
// The amount of data (post block alignment) written thus far to the container.
AtomicInt<int64_t> total_bytes_;
// The number of blocks written thus far in the container.
AtomicInt<int64_t> total_blocks_;
// The amount of data present in not-yet-deleted blocks of the container.
AtomicInt<int64_t> live_bytes_;
// The amount of data (post block alignment) present in not-yet-deleted
// blocks of the container.
AtomicInt<int64_t> live_bytes_aligned_;
// The number of not-yet-deleted blocks in the container.
AtomicInt<int64_t> live_blocks_;
// The number of LogWritableBlocks currently open for this container.
AtomicInt<int32_t> blocks_being_written_;
// Whether or not this container has been marked as dead.
AtomicBool dead_;
// The metrics. Not owned by the log container; it has the same lifespan
// as the block manager.
const LogBlockManagerMetrics* metrics_;
// If true, only read operations are allowed. Existing blocks may
// not be deleted until the next restart, and new blocks may not
// be added.
mutable simple_spinlock read_only_lock_;
Status read_only_status_;
DISALLOW_COPY_AND_ASSIGN(LogBlockContainer);
};
#define CONTAINER_DISK_FAILURE(status_expr, msg) do { \
Status s_ = (status_expr); \
HANDLE_DISK_FAILURE(s_, block_manager_->error_manager_->RunErrorNotificationCb( \
ErrorHandlerType::DISK_ERROR, data_dir_)); \
WARN_NOT_OK(s_, msg); \
} while (0)
LogBlockContainer::LogBlockContainer(
LogBlockManager* block_manager,
Dir* data_dir,
unique_ptr<WritablePBContainerFile> metadata_file,
shared_ptr<RWFile> data_file)
: block_manager_(block_manager),
data_dir_(data_dir),
max_num_blocks_(FindOrDie(block_manager->block_limits_by_data_dir_,
data_dir)),
metadata_file_(std::move(metadata_file)),
data_file_(std::move(data_file)),
next_block_offset_(0),
total_bytes_(0),
total_blocks_(0),
live_bytes_(0),
live_bytes_aligned_(0),
live_blocks_(0),
blocks_being_written_(0),
dead_(false),
metrics_(block_manager->metrics()) {
}
LogBlockContainer::~LogBlockContainer() {
if (dead()) {
CHECK(!block_manager_->opts_.read_only);
string data_file_name = data_file_->filename();
string metadata_file_name = metadata_file_->filename();
string data_failure_msg =
"Could not delete dead container data file " + data_file_name;
string metadata_failure_msg =
"Could not delete dead container metadata file " + metadata_file_name;
if (PREDICT_TRUE(block_manager_->file_cache_)) {
CONTAINER_DISK_FAILURE(block_manager_->file_cache_->DeleteFile(data_file_name),
data_failure_msg);
CONTAINER_DISK_FAILURE(block_manager_->file_cache_->DeleteFile(metadata_file_name),
metadata_failure_msg);
} else {
CONTAINER_DISK_FAILURE(block_manager_->env_->DeleteFile(data_file_name),
data_failure_msg);
CONTAINER_DISK_FAILURE(block_manager_->env_->DeleteFile(metadata_file_name),
metadata_failure_msg);
}
}
}
void LogBlockContainer::HandleError(const Status& s) const {
HANDLE_DISK_FAILURE(s,
block_manager()->error_manager()->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR,
data_dir_));
}
#define RETURN_NOT_OK_CONTAINER_DISK_FAILURE(status_expr) do { \
RETURN_NOT_OK_HANDLE_DISK_FAILURE((status_expr), \
block_manager->error_manager()->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR, dir)); \
} while (0)
Status LogBlockContainer::Create(LogBlockManager* block_manager,
Dir* dir,
LogBlockContainerRefPtr* container) {
string common_path;
string metadata_path;
string data_path;
Status metadata_status;
Status data_status;
shared_ptr<RWFile> metadata_writer;
shared_ptr<RWFile> data_file;
// Repeat in the event of a container id collision (unlikely).
//
// When looping, we delete any created-and-orphaned files.
do {
common_path = JoinPathSegments(dir->dir(),
block_manager->oid_generator()->Next());
metadata_path = StrCat(common_path, LogBlockManager::kContainerMetadataFileSuffix);
data_path = StrCat(common_path, LogBlockManager::kContainerDataFileSuffix);
if (PREDICT_TRUE(block_manager->file_cache_)) {
if (metadata_writer) {
WARN_NOT_OK(block_manager->file_cache_->DeleteFile(metadata_path),
"could not delete orphaned metadata file thru file cache");
}
if (data_file) {
WARN_NOT_OK(block_manager->file_cache_->DeleteFile(data_path),
"could not delete orphaned data file thru file cache");
}
metadata_status = block_manager->file_cache_->OpenFile<Env::MUST_CREATE>(
metadata_path, &metadata_writer);
data_status = block_manager->file_cache_->OpenFile<Env::MUST_CREATE>(
data_path, &data_file);
} else {
if (metadata_writer) {
WARN_NOT_OK(block_manager->env()->DeleteFile(metadata_path),
"could not delete orphaned metadata file");
}
if (data_file) {
WARN_NOT_OK(block_manager->env()->DeleteFile(data_path),
"could not delete orphaned data file");
}
unique_ptr<RWFile> rwf;
RWFileOptions rw_opts;
rw_opts.mode = Env::MUST_CREATE;
metadata_status = block_manager->env()->NewRWFile(
rw_opts, metadata_path, &rwf);
metadata_writer.reset(rwf.release());
data_status = block_manager->env()->NewRWFile(
rw_opts, data_path, &rwf);
data_file.reset(rwf.release());
}
} while (PREDICT_FALSE(metadata_status.IsAlreadyPresent() ||
data_status.IsAlreadyPresent()));
if (metadata_status.ok() && data_status.ok()) {
unique_ptr<WritablePBContainerFile> metadata_file(new WritablePBContainerFile(
std::move(metadata_writer)));
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(metadata_file->CreateNew(BlockRecordPB()));
container->reset(new LogBlockContainer(block_manager,
dir,
std::move(metadata_file),
std::move(data_file)));
VLOG(1) << "Created log block container " << (*container)->ToString();
}
// Prefer metadata status (arbitrarily).
FsErrorManager* em = block_manager->error_manager();
HANDLE_DISK_FAILURE(metadata_status, em->RunErrorNotificationCb(
ErrorHandlerType::DISK_ERROR, dir));
HANDLE_DISK_FAILURE(data_status, em->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR, dir));
return !metadata_status.ok() ? metadata_status : data_status;
}
Status LogBlockContainer::Open(LogBlockManager* block_manager,
Dir* dir,
FsReport* report,
const string& id,
LogBlockContainerRefPtr* container) {
string common_path = JoinPathSegments(dir->dir(), id);
string data_path = StrCat(common_path, LogBlockManager::kContainerDataFileSuffix);
string metadata_path = StrCat(common_path, LogBlockManager::kContainerMetadataFileSuffix);
RETURN_NOT_OK(CheckContainerFiles(block_manager, report, dir,
common_path, data_path, metadata_path));
// Open the existing metadata and data files for writing.
shared_ptr<RWFile> metadata_file;
shared_ptr<RWFile> data_file;
if (PREDICT_TRUE(block_manager->file_cache_)) {
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(
block_manager->file_cache_->OpenFile<Env::MUST_EXIST>(metadata_path, &metadata_file));
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(
block_manager->file_cache_->OpenFile<Env::MUST_EXIST>(data_path, &data_file));
} else {
RWFileOptions opts;
opts.mode = Env::MUST_EXIST;
unique_ptr<RWFile> rwf;
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(block_manager->env()->NewRWFile(opts,
metadata_path, &rwf));
metadata_file.reset(rwf.release());
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(block_manager->env()->NewRWFile(opts,
data_path, &rwf));
data_file.reset(rwf.release());
}
unique_ptr<WritablePBContainerFile> metadata_pb_writer;
metadata_pb_writer.reset(new WritablePBContainerFile(std::move(metadata_file)));
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(metadata_pb_writer->OpenExisting());
uint64_t data_file_size;
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(data_file->Size(&data_file_size));
// Create the in-memory container and populate it.
LogBlockContainerRefPtr open_container(new LogBlockContainer(block_manager,
dir,
std::move(metadata_pb_writer),
std::move(data_file)));
open_container->preallocated_offset_ = data_file_size;
VLOG(1) << "Opened log block container " << open_container->ToString();
container->swap(open_container);
return Status::OK();
}
Status LogBlockContainer::CheckContainerFiles(LogBlockManager* block_manager,
FsReport* report,
const Dir* dir,
const string& common_path,
const string& data_path,
const string& metadata_path) {
Env* env = block_manager->env();
uint64_t data_size = 0;
Status s_data = env->GetFileSize(data_path, &data_size);
if (!s_data.ok() && !s_data.IsNotFound()) {
s_data = s_data.CloneAndPrepend("unable to determine data file size");
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(s_data);
}
uint64_t metadata_size = 0;
Status s_meta = env->GetFileSize(metadata_path, &metadata_size);
if (!s_meta.ok() && !s_meta.IsNotFound()) {
s_meta = s_meta.CloneAndPrepend("unable to determine metadata file size");
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(s_meta);
}
// Check that both the metadata and data files exist and have valid lengths.
// This covers a commonly seen case at startup, where the previous incarnation
// of the server crashed due to "too many open files" just as it was trying
// to create a file. This orphans an empty or invalid length file, which we can
// safely delete. And another case is that the metadata and data files exist,
// but the lengths are invalid.
if (PREDICT_FALSE(metadata_size < pb_util::kPBContainerMinimumValidLength &&
data_size == 0)) {
report->incomplete_container_check->entries.emplace_back(common_path);
return Status::Aborted(Substitute("orphaned empty or invalid length file $0", common_path));
}
// Handle a half-present container whose data file has gone missing and
// the metadata file has no live blocks. If that's true, the (orphaned)
// metadata file will be deleted when repairing.
//
// Open the metadata file and quickly check whether or not there is any live blocks.
if (PREDICT_FALSE(metadata_size >= pb_util::kPBContainerMinimumValidLength &&
s_data.IsNotFound())) {
Status read_status;
BlockIdSet live_blocks;
unique_ptr<RandomAccessFile> reader;
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(env->NewRandomAccessFile(metadata_path, &reader));
ReadablePBContainerFile pb_reader(std::move(reader));
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(pb_reader.Open());
while (true) {
BlockRecordPB record;
read_status = pb_reader.ReadNextPB(&record);
if (!read_status.ok()) break;
switch (record.op_type()) {
case CREATE:
live_blocks.emplace(BlockId::FromPB(record.block_id()));
break;
case DELETE:
live_blocks.erase(BlockId::FromPB(record.block_id()));
break;
default:
LOG(WARNING) << Substitute("Found a record with unknown type $0", record.op_type());
break;
}
}
if (read_status.IsEndOfFile() && live_blocks.empty()) {
report->incomplete_container_check->entries.emplace_back(common_path);
return Status::Aborted(Substitute("orphaned metadata file with no live blocks $0",
common_path));
}
// If the read failed for some unexpected reason, propagate the error.
if (!read_status.IsEndOfFile() && !read_status.IsIncomplete()) {
RETURN_NOT_OK_CONTAINER_DISK_FAILURE(read_status);
}
}
// Except the special cases above, returns error status if any.
if (s_data.IsNotFound()) RETURN_NOT_OK_CONTAINER_DISK_FAILURE(s_data);
if (s_meta.IsNotFound()) RETURN_NOT_OK_CONTAINER_DISK_FAILURE(s_meta);
return Status::OK();
}
Status LogBlockContainer::TruncateDataToNextBlockOffset() {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
if (full()) {
VLOG(2) << Substitute("Truncating container $0 to offset $1",
ToString(), next_block_offset());
RETURN_NOT_OK_HANDLE_ERROR(data_file_->Truncate(next_block_offset()));
}
return Status::OK();
}
Status LogBlockContainer::ProcessRecords(
FsReport* report,
LogBlockManager::UntrackedBlockMap* live_blocks,
LogBlockManager::BlockRecordMap* live_block_records,
vector<LogBlockRefPtr>* dead_blocks,
uint64_t* max_block_id) {
string metadata_path = metadata_file_->filename();
unique_ptr<RandomAccessFile> metadata_reader;
RETURN_NOT_OK_HANDLE_ERROR(block_manager()->env()->NewRandomAccessFile(
metadata_path, &metadata_reader));
ReadablePBContainerFile pb_reader(std::move(metadata_reader));
RETURN_NOT_OK_HANDLE_ERROR(pb_reader.Open());
uint64_t data_file_size = 0;
Status read_status;
while (true) {
BlockRecordPB record;
read_status = pb_reader.ReadNextPB(&record);
if (!read_status.ok()) {
break;
}
RETURN_NOT_OK(ProcessRecord(&record, report,
live_blocks, live_block_records, dead_blocks,
&data_file_size, max_block_id));
}
// NOTE: 'read_status' will never be OK here.
if (PREDICT_TRUE(read_status.IsEndOfFile())) {
// We've reached the end of the file without any problems.
return Status::OK();
}
if (read_status.IsIncomplete()) {
// We found a partial trailing record in a version of the pb container file
// format that can reliably detect this. Consider this a failed partial
// write and truncate the metadata file to remove this partial record.
report->partial_record_check->entries.emplace_back(ToString(),
pb_reader.offset());
return Status::OK();
}
// If we've made it here, we've found (and are returning) an unrecoverable error.
// Handle any errors we can, e.g. disk failures.
HandleError(read_status);
return read_status;
}
Status LogBlockContainer::ProcessRecord(
BlockRecordPB* record,
FsReport* report,
LogBlockManager::UntrackedBlockMap* live_blocks,
LogBlockManager::BlockRecordMap* live_block_records,
vector<LogBlockRefPtr>* dead_blocks,
uint64_t* data_file_size,
uint64_t* max_block_id) {
const BlockId block_id(BlockId::FromPB(record->block_id()));
LogBlockRefPtr lb;
switch (record->op_type()) {
case CREATE:
// First verify that the record's offset/length aren't wildly incorrect.
if (PREDICT_FALSE(!record->has_offset() ||
!record->has_length() ||
record->offset() < 0 ||
record->length() < 0)) {
report->malformed_record_check->entries.emplace_back(ToString(), record);
break;
}
// Now it should be safe to access the record's offset/length.
//
// KUDU-1657: When opening a container in read-only mode which is actively
// being written to by another lbm, we must reinspect the data file's size
// frequently in order to account for the latest appends. Inspecting the
// file size is expensive, so we only do it when the metadata indicates
// that additional data has been written to the file.
if (PREDICT_FALSE(record->offset() + record->length() > *data_file_size)) {
RETURN_NOT_OK_HANDLE_ERROR(data_file_->Size(data_file_size));
}
// If the record still extends beyond the end of the file, it is malformed.
if (PREDICT_FALSE(record->offset() + record->length() > *data_file_size)) {
// TODO(adar): treat as a different kind of inconsistency?
report->malformed_record_check->entries.emplace_back(ToString(), record);
break;
}
lb = new LogBlock(this, block_id, record->offset(), record->length());
if (!InsertIfNotPresent(live_blocks, block_id, lb)) {
// We found a record whose ID matches that of an already created block.
//
// TODO(adar): treat as a different kind of inconsistency?
report->malformed_record_check->entries.emplace_back(
ToString(), record);
break;
}
VLOG(2) << Substitute("Found CREATE block $0 at offset $1 with length $2",
block_id.ToString(),
record->offset(), record->length());
// This block must be included in the container's logical size, even if
// it has since been deleted. This helps satisfy one of our invariants:
// once a container byte range has been used, it may never be reused in
// the future.
//
// If we ignored deleted blocks, we would end up reusing the space
// belonging to the last deleted block in the container.
UpdateNextBlockOffset(lb->offset(), lb->length());
BlockCreated(lb);
(*live_block_records)[block_id].Swap(record);
*max_block_id = std::max(*max_block_id, block_id.id());
break;
case DELETE:
lb = EraseKeyReturnValuePtr(live_blocks, block_id);
if (!lb) {
// We found a record for which there is no already created block.
//
// TODO(adar): treat as a different kind of inconsistency?
report->malformed_record_check->entries.emplace_back(ToString(), record);
break;
}
VLOG(2) << Substitute("Found DELETE block $0", block_id.ToString());
BlockDeleted(lb);
CHECK_EQ(1, live_block_records->erase(block_id));
dead_blocks->emplace_back(std::move(lb));
break;
default:
// We found a record with an unknown type.
//
// TODO(adar): treat as a different kind of inconsistency?
report->malformed_record_check->entries.emplace_back(ToString(), record);
break;
}
return Status::OK();
}
Status LogBlockContainer::DoCloseBlocks(const vector<LogWritableBlock*>& blocks,
SyncMode mode) {
auto sync_blocks = [&]() {
if (mode == SYNC) {
VLOG(3) << "Syncing data file " << data_file_->filename();
RETURN_NOT_OK(SyncData());
}
// Append metadata only after data is synced so that there's
// no chance of metadata landing on the disk before the data.
for (auto* block : blocks) {
RETURN_NOT_OK_PREPEND(block->AppendMetadata(),
"unable to append block's metadata during close");
}
if (mode == SYNC) {
VLOG(3) << "Syncing metadata file " << metadata_file_->filename();
RETURN_NOT_OK(SyncMetadata());
}
RETURN_NOT_OK(block_manager()->SyncContainer(*this));
for (LogWritableBlock* block : blocks) {
if (blocks.size() > 1) DCHECK_EQ(block->state(), WritableBlock::State::FINALIZED);
block->DoClose();
}
return Status::OK();
};
Status s = sync_blocks();
if (!s.ok()) {
// Make container read-only to forbid further writes in case of failure.
// Because the on-disk state may contain partial/incomplete data/metadata at
// this point, it is not safe to either overwrite it or append to it.
SetReadOnly(s);
}
return s;
}
Status LogBlockContainer::PunchHole(int64_t offset, int64_t length) {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
DCHECK_GE(offset, 0);
DCHECK_GE(length, 0);
// It is invalid to punch a zero-size hole.
if (length) {
// It's OK if we exceed the file's total size; the kernel will truncate
// our request.
RETURN_NOT_OK_HANDLE_ERROR(data_file_->PunchHole(offset, length));
}
return Status::OK();
}
Status LogBlockContainer::WriteData(int64_t offset, const Slice& data) {
return WriteVData(offset, ArrayView<const Slice>(&data, 1));
}
Status LogBlockContainer::WriteVData(int64_t offset, ArrayView<const Slice> data) {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
DCHECK_GE(offset, next_block_offset());
RETURN_NOT_OK_HANDLE_ERROR(data_file_->WriteV(offset, data));
// This append may have changed the container size if:
// 1. It was large enough that it blew out the preallocated space.
// 2. Preallocation was disabled.
size_t data_size = accumulate(data.begin(), data.end(), static_cast<size_t>(0),
[&](int sum, const Slice& curr) {
return sum + curr.size();
});
if (offset + data_size > preallocated_offset_) {
RETURN_NOT_OK_HANDLE_ERROR(data_dir_->RefreshAvailableSpace(Dir::RefreshMode::ALWAYS));
}
return Status::OK();
}
Status LogBlockContainer::ReadData(int64_t offset, Slice result) const {
DCHECK_GE(offset, 0);
RETURN_NOT_OK_HANDLE_ERROR(data_file_->Read(offset, result));
return Status::OK();
}
Status LogBlockContainer::ReadVData(int64_t offset, ArrayView<Slice> results) const {
DCHECK_GE(offset, 0);
RETURN_NOT_OK_HANDLE_ERROR(data_file_->ReadV(offset, results));
return Status::OK();
}
Status LogBlockContainer::AppendMetadata(const BlockRecordPB& pb) {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
// Note: We don't check for sufficient disk space for metadata writes in
// order to allow for block deletion on full disks.
RETURN_NOT_OK_HANDLE_ERROR(metadata_file_->Append(pb));
return Status::OK();
}
Status LogBlockContainer::FlushData(int64_t offset, int64_t length) {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
DCHECK_GE(offset, 0);
DCHECK_GE(length, 0);
RETURN_NOT_OK_HANDLE_ERROR(data_file_->Flush(RWFile::FLUSH_ASYNC, offset, length));
return Status::OK();
}
Status LogBlockContainer::FlushMetadata() {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
RETURN_NOT_OK_HANDLE_ERROR(metadata_file_->Flush());
return Status::OK();
}
Status LogBlockContainer::SyncData() {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
if (FLAGS_enable_data_block_fsync) {
if (metrics_) metrics_->generic_metrics.total_disk_sync->Increment();
RETURN_NOT_OK_HANDLE_ERROR(data_file_->Sync());
}
return Status::OK();
}
Status LogBlockContainer::SyncMetadata() {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
if (FLAGS_enable_data_block_fsync) {
if (metrics_) metrics_->generic_metrics.total_disk_sync->Increment();
RETURN_NOT_OK_HANDLE_ERROR(metadata_file_->Sync());
}
return Status::OK();
}
Status LogBlockContainer::ReopenMetadataWriter() {
shared_ptr<RWFile> f;
if (PREDICT_TRUE(block_manager_->file_cache_)) {
RETURN_NOT_OK_HANDLE_ERROR(block_manager_->file_cache_->OpenFile<Env::MUST_EXIST>(
metadata_file_->filename(), &f));
} else {
unique_ptr<RWFile> f_uniq;
RWFileOptions opts;
opts.mode = Env::MUST_EXIST;
RETURN_NOT_OK_HANDLE_ERROR(block_manager_->env_->NewRWFile(opts,
metadata_file_->filename(), &f_uniq));
f.reset(f_uniq.release());
}
unique_ptr<WritablePBContainerFile> w;
w.reset(new WritablePBContainerFile(std::move(f)));
RETURN_NOT_OK_HANDLE_ERROR(w->OpenExisting());
RETURN_NOT_OK_HANDLE_ERROR(metadata_file_->Close());
metadata_file_.swap(w);
return Status::OK();
}
Status LogBlockContainer::EnsurePreallocated(int64_t block_start_offset,
size_t next_append_length) {
RETURN_NOT_OK_HANDLE_ERROR(read_only_status());
DCHECK_GE(block_start_offset, 0);
if (!FLAGS_log_container_preallocate_bytes) {
return Status::OK();
}
// If the last write blew out the preallocation window, or if the next write
// exceeds it, we need to preallocate another chunk.
if (block_start_offset > preallocated_offset_ ||
next_append_length > preallocated_offset_ - block_start_offset) {
int64_t off = std::max(preallocated_offset_, block_start_offset);
int64_t len = FLAGS_log_container_preallocate_bytes;
RETURN_NOT_OK_HANDLE_ERROR(data_file_->PreAllocate(off, len, RWFile::CHANGE_FILE_SIZE));
RETURN_NOT_OK_HANDLE_ERROR(data_dir_->RefreshAvailableSpace(Dir::RefreshMode::ALWAYS));
VLOG(2) << Substitute("Preallocated $0 bytes at offset $1 in container $2",
len, off, ToString());
preallocated_offset_ = off + len;
}
return Status::OK();
}
void LogBlockContainer::FinalizeBlock(int64_t block_offset, int64_t block_length) {
// Updates this container's next block offset before marking it as available
// to ensure thread safety while updating internal bookkeeping state.
UpdateNextBlockOffset(block_offset, block_length);
// Truncate the container if it's now full; any left over preallocated space
// is no longer needed.
//
// Note that depending on when FinalizeBlock() is called, this can take place
// _after_ the container has been synced to disk. That's OK; truncation isn't
// needed for correctness, and in the event of a crash or error, it will be
// retried at startup.
WARN_NOT_OK(TruncateDataToNextBlockOffset(),
"could not truncate excess preallocated space");
if (full() && block_manager_->metrics()) {
block_manager_->metrics()->full_containers->Increment();
}
block_manager_->MakeContainerAvailable(this);
}
void LogBlockContainer::UpdateNextBlockOffset(int64_t block_offset, int64_t block_length) {
DCHECK_GE(block_offset, 0);
// The log block manager maintains block contiguity as an invariant, which
// means accounting for the new block should be as simple as adding its
// aligned length to 'next_block_offset_'. However, due to KUDU-1793, some
// containers may have developed gaps between blocks. We'll account for them
// by considering both the block's offset and its length.
//
// The number of bytes is rounded up to the nearest filesystem block so
// that each Kudu block is guaranteed to be on a filesystem block
// boundary. This guarantees that the disk space can be reclaimed when
// the block is deleted.
int64_t new_next_block_offset = KUDU_ALIGN_UP(
block_offset + block_length,
instance()->filesystem_block_size_bytes());
next_block_offset_.StoreMax(new_next_block_offset);
if (full()) {
VLOG(1) << Substitute(
"Container $0 with size $1 is now full, max size is $2",
ToString(), next_block_offset(), FLAGS_log_container_max_size);
}
}
void LogBlockContainer::BlockCreated(const LogBlockRefPtr& block) {
DCHECK_GE(block->offset(), 0);
total_bytes_.IncrementBy(block->fs_aligned_length());
total_blocks_.Increment();
live_bytes_.IncrementBy(block->length());
live_bytes_aligned_.IncrementBy(block->fs_aligned_length());
live_blocks_.Increment();
}
void LogBlockContainer::BlockDeleted(const LogBlockRefPtr& block) {
DCHECK_GE(block->offset(), 0);
live_bytes_.IncrementBy(-block->length());
live_bytes_aligned_.IncrementBy(-block->fs_aligned_length());
live_blocks_.IncrementBy(-1);
}
void LogBlockContainer::ExecClosure(const std::function<void()>& task) {
data_dir_->ExecClosure(task);
}
string LogBlockContainer::ToString() const {
string s;
CHECK(TryStripSuffixString(data_file_->filename(),
LogBlockManager::kContainerDataFileSuffix, &s));
return s;
}
void LogBlockContainer::SetReadOnly(const Status& error) {
DCHECK(!error.ok());
LOG(WARNING) << Substitute("Container $0 being marked read-only: $1",
ToString(), error.ToString());
std::lock_guard<simple_spinlock> l(read_only_lock_);
read_only_status_ = error;
}
void LogBlockContainer::ContainerDeletionAsync(int64_t offset, int64_t length) {
if (dead()) {
// Don't bother punching holes; the container's destructor will delete the
// container's files outright.
return;
}
VLOG(3) << "Freeing space belonging to container " << ToString();
Status s = PunchHole(offset, length);
if (s.ok() && metrics_) metrics_->holes_punched->Increment();
WARN_NOT_OK(s, Substitute("could not delete blocks in container $0",
data_dir()->dir()));
}
///////////////////////////////////////////////////////////
// LogBlockCreationTransaction
////////////////////////////////////////////////////////////
class LogBlockCreationTransaction : public BlockCreationTransaction {
public:
LogBlockCreationTransaction() = default;
virtual ~LogBlockCreationTransaction() = default;
virtual void AddCreatedBlock(unique_ptr<WritableBlock> block) override;
virtual Status CommitCreatedBlocks() override;
private:
vector<unique_ptr<LogWritableBlock>> created_blocks_;
};
void LogBlockCreationTransaction::AddCreatedBlock(
unique_ptr<WritableBlock> block) {
LogWritableBlock* lwb = down_cast<LogWritableBlock*>(block.release());
created_blocks_.emplace_back(unique_ptr<LogWritableBlock>(lwb));
}
Status LogBlockCreationTransaction::CommitCreatedBlocks() {
if (created_blocks_.empty()) {
return Status::OK();
}
VLOG(3) << "Closing " << created_blocks_.size() << " blocks";
unordered_map<LogBlockContainer*, vector<LogWritableBlock*>> created_block_map;
for (const auto& block : created_blocks_) {
if (FLAGS_block_manager_preflush_control == "close") {
// Ask the kernel to begin writing out each block's dirty data. This is
// done up-front to give the kernel opportunities to coalesce contiguous
// dirty pages.
RETURN_NOT_OK(block->FlushDataAsync());
}
created_block_map[block->container()].emplace_back(block.get());
}
// Close all blocks and sync the blocks belonging to the same
// container together to reduce fsync() usage, waiting for them
// to become durable.
for (const auto& entry : created_block_map) {
RETURN_NOT_OK(entry.first->DoCloseBlocks(entry.second,
LogBlockContainer::SyncMode::SYNC));
}
created_blocks_.clear();
return Status::OK();
}
///////////////////////////////////////////////////////////
// LogBlockDeletionTransaction
////////////////////////////////////////////////////////////
class LogBlockDeletionTransaction : public BlockDeletionTransaction,
public std::enable_shared_from_this<LogBlockDeletionTransaction> {
public:
explicit LogBlockDeletionTransaction(LogBlockManager* lbm)
: lbm_(lbm) {
}
// Due to the shared ownership of LogBlockDeletionTransaction,
// the destructor is responsible for destroying all registered
// blocks. This includes:
// 1. Punching holes in deleted blocks, and
// 2. Deleting dead containers outright.
virtual ~LogBlockDeletionTransaction();
virtual void AddDeletedBlock(BlockId block) override;
virtual Status CommitDeletedBlocks(vector<BlockId>* deleted) override;
// Add the given block that needs to be deleted to 'deleted_interval_map_',
// which keeps track of container and the range to be hole punched.
void AddBlock(const LogBlockRefPtr& lb);
private:
// Block <offset, offset + length> pair.
typedef std::pair<int64_t, int64_t> BlockInterval;
// Map used to aggregate BlockInterval instances across containers.
unordered_map<LogBlockContainerRefPtr,
vector<BlockInterval>,
ScopedRefPtrHashFunctor<LogBlockContainer>,
ScopedRefPtrEqualToFunctor<LogBlockContainer>> deleted_interval_map_;
// The owning LogBlockManager. Must outlive the LogBlockDeletionTransaction.
LogBlockManager* lbm_;
vector<BlockId> deleted_blocks_;
DISALLOW_COPY_AND_ASSIGN(LogBlockDeletionTransaction);
};
void LogBlockDeletionTransaction::AddDeletedBlock(BlockId block) {
deleted_blocks_.emplace_back(block);
}
LogBlockDeletionTransaction::~LogBlockDeletionTransaction() {
for (auto& entry : deleted_interval_map_) {
LogBlockContainer* container = entry.first.get();
// For the full and dead containers, it is much cheaper
// to delete the container files outright, rather than
// punching holes.
if (container->check_death_condition()) {
// Mark the container as deleted and remove it from the global map.
//
// It's possible for multiple deletion transactions to end up here. For
// example, if one transaction deletes the second to last block in a
// container and the second deletes its last block, the destructors of
// both transactions could run at a time that container's internal
// bookkeeping reflects the deadness of the container.
//
// The function 'TrySetDead()' can only be called successfully once,
// because there is a compare-and-swap operation inside.
if (container->TrySetDead()) {
lbm_->RemoveDeadContainer(container->ToString());
}
continue;
}
CHECK_OK_PREPEND(CoalesceIntervals<int64_t>(&entry.second),
Substitute("could not coalesce hole punching for container: $0",
container->ToString()));
scoped_refptr<LogBlockContainer> self(container);
for (const auto& interval : entry.second) {
container->ExecClosure([self, interval]() {
self->ContainerDeletionAsync(interval.first, interval.second - interval.first);
});
}
}
}
Status LogBlockDeletionTransaction::CommitDeletedBlocks(vector<BlockId>* deleted) {
deleted->clear();
shared_ptr<LogBlockDeletionTransaction> transaction = shared_from_this();
vector<LogBlockRefPtr> log_blocks;
Status first_failure = lbm_->RemoveLogBlocks(deleted_blocks_, &log_blocks, deleted);
for (const auto& lb : log_blocks) {
// Register the block to be hole punched if metadata recording
// is successful.
lb->RegisterDeletion(transaction);
AddBlock(lb);
if (lbm_->metrics_) {
lbm_->metrics_->generic_metrics.total_blocks_deleted->Increment();
}
}
if (!first_failure.ok()) {
first_failure = first_failure.CloneAndPrepend(Substitute("only deleted $0 blocks, "
"first failure", deleted->size()));
}
deleted_blocks_.clear();
return first_failure;
}
void LogBlockDeletionTransaction::AddBlock(const LogBlockRefPtr& lb) {
DCHECK_GE(lb->fs_aligned_length(), 0);
BlockInterval block_interval(lb->offset(),
lb->offset() + lb->fs_aligned_length());
deleted_interval_map_[lb->container()].emplace_back(block_interval);
}
////////////////////////////////////////////////////////////
// LogBlockContainerLoadResult
////////////////////////////////////////////////////////////
struct LogBlockContainerLoadResult {
Status status;
FsReport report;
// Keep track of containers that have nothing but dead blocks; they will be
// deleted during repair.
vector<LogBlockContainerRefPtr> dead_containers;
// Keep track of containers whose live block ratio is low; their metadata
// files will be compacted during repair.
unordered_map<string, vector<BlockRecordPB>> low_live_block_containers;
// Keep track of deleted blocks whose space hasn't been punched; they will
// be repunched during repair.
vector<LogBlockRefPtr> need_repunching_blocks;
LogBlockContainerLoadResult() {
// We are going to perform these checks.
//
// Note: this isn't necessarily the complete set of FsReport checks; there
// may be checks that the LBM cannot perform.
report.full_container_space_check.emplace();
report.incomplete_container_check.emplace();
report.malformed_record_check.emplace();
report.misaligned_block_check.emplace();
report.partial_record_check.emplace();
}
};
////////////////////////////////////////////////////////////
// LogBlock (definition)
////////////////////////////////////////////////////////////
LogBlock::LogBlock(LogBlockContainerRefPtr container, BlockId block_id,
int64_t offset, int64_t length)
: container_(std::move(container)),
block_id_(block_id),
offset_(offset),
length_(length) {
DCHECK_GE(offset, 0);
DCHECK_GE(length, 0);
}
int64_t LogBlock::fs_aligned_length() const {
uint64_t fs_block_size = container_->instance()->filesystem_block_size_bytes();
// Nearly all blocks are placed on a filesystem block boundary, which means
// their length post-alignment is simply their length aligned up to the
// nearest fs block size.
//
// However, due to KUDU-1793, some blocks may start or end at misaligned
// offsets. We don't maintain enough state to precisely pinpoint such a
// block's (aligned) end offset in this case, so we'll just undercount it.
// This should be safe, although it may mean unreclaimed disk space (i.e.
// when fs_aligned_length() is used in hole punching).
if (PREDICT_TRUE(offset_ % fs_block_size == 0)) {
return KUDU_ALIGN_UP(length_, fs_block_size);
}
return length_;
}
void LogBlock::RegisterDeletion(
const shared_ptr<LogBlockDeletionTransaction>& transaction) {
DCHECK(!transaction_);
DCHECK(transaction);
transaction_ = transaction;
}
////////////////////////////////////////////////////////////
// LogWritableBlock (definition)
////////////////////////////////////////////////////////////
LogWritableBlock::LogWritableBlock(LogBlockContainerRefPtr container,
BlockId block_id, int64_t block_offset)
: container_(std::move(container)),
block_id_(block_id),
block_offset_(block_offset),
block_length_(0),
state_(CLEAN) {
DCHECK_GE(block_offset, 0);
DCHECK_EQ(0, block_offset % container_->instance()->filesystem_block_size_bytes());
container_->blocks_being_written_incr(1);
if (container_->metrics()) {
container_->metrics()->generic_metrics.blocks_open_writing->Increment();
container_->metrics()->generic_metrics.total_writable_blocks->Increment();
}
}
LogWritableBlock::~LogWritableBlock() {
// Put the decrement 'blocks_being_written_' at the beginning of this
// function can help to avoid unnecessary hole punch.
container_->blocks_being_written_incr(-1);
if (state_ != CLOSED) {
WARN_NOT_OK(Abort(), Substitute("Failed to abort block $0",
id().ToString()));
}
}
Status LogWritableBlock::Close() {
return container_->DoCloseBlocks({ this }, LogBlockContainer::SyncMode::SYNC);
}
Status LogWritableBlock::Abort() {
// Only updates metrics and block state for read-only container.
if (container_->read_only()) {
if (state_ != CLOSED) {
state_ = CLOSED;
if (container_->metrics()) {
container_->metrics()->generic_metrics.blocks_open_writing->Decrement();
container_->metrics()->generic_metrics.total_bytes_written->IncrementBy(
block_length_);
}
}
return container_->read_only_status().CloneAndPrepend(
Substitute("container $0 is read-only", container_->ToString()));
}
// Close the block and then delete it. Theoretically, we could do nothing
// for Abort() other than updating metrics and block state. Here is the
// reasoning why it would be safe to do so. Currently, failures in block
// creation can happen at various stages:
// 1) before the block is finalized (either in CLEAN or DIRTY state). It is
// safe to do nothing, as the block is not yet finalized and next block
// will overwrite the dirty data.
// 2) after the block is finalized but before the metadata is successfully
// appended. That means the container's internal bookkeeping has been
// updated and the data could be durable. Doing nothing can result in
// data-consuming gaps in containers, but these gaps can be cleaned up
// by hole repunching at start up.
// 3) after metadata is appended. If we do nothing, this can result in
// orphaned blocks if the metadata is durable. But orphaned blocks can be
// garbage collected later.
//
// TODO(Hao): implement a fine-grained abort handling.
// However it is better to provide a fine-grained abort handling to
// avoid large chunks of data-consuming gaps and orphaned blocks. A
// possible way to do so is,
// 1) for per block abort, if the block state is FINALIZED, do hole
// punching.
// 2) for BlockTransaction abort, DoCloseBlock() should append metadata
// for deletion and coalescing hole punching for blocks in the same
// container.
RETURN_NOT_OK(container_->DoCloseBlocks({ this }, LogBlockContainer::SyncMode::NO_SYNC));
// DoCloseBlocks() has unlocked the container; it may be locked by someone else.
// But block_manager_ is immutable, so this is safe.
shared_ptr<BlockDeletionTransaction> deletion_transaction =
container_->block_manager()->NewDeletionTransaction();
deletion_transaction->AddDeletedBlock(id());
vector<BlockId> deleted;
return deletion_transaction->CommitDeletedBlocks(&deleted);
}
const BlockId& LogWritableBlock::id() const {
return block_id_;
}
BlockManager* LogWritableBlock::block_manager() const {
return container_->block_manager();
}
Status LogWritableBlock::Append(const Slice& data) {
return AppendV(ArrayView<const Slice>(&data, 1));
}
Status LogWritableBlock::AppendV(ArrayView<const Slice> data) {
DCHECK(state_ == CLEAN || state_ == DIRTY)
<< "Invalid state: " << state_;
// Calculate the amount of data to write
size_t data_size = accumulate(data.begin(), data.end(), static_cast<size_t>(0),
[&](int sum, const Slice& curr) {
return sum + curr.size();
});
// The metadata change is deferred to Close(). We can't do
// it now because the block's length is still in flux.
int64_t cur_block_offset = block_offset_ + block_length_;
RETURN_NOT_OK(container_->EnsurePreallocated(cur_block_offset, data_size));
MicrosecondsInt64 start_time = GetMonoTimeMicros();
RETURN_NOT_OK(container_->WriteVData(cur_block_offset, data));
MicrosecondsInt64 end_time = GetMonoTimeMicros();
int64_t dur = end_time - start_time;
TRACE_COUNTER_INCREMENT("lbm_write_time_us", dur);
const char* counter = BUCKETED_COUNTER_NAME("lbm_writes", dur);
TRACE_COUNTER_INCREMENT(counter, 1);
block_length_ += data_size;
state_ = DIRTY;
return Status::OK();
}
Status LogWritableBlock::FlushDataAsync() {
VLOG(3) << "Flushing block " << id();
RETURN_NOT_OK(container_->FlushData(block_offset_, block_length_));
return Status::OK();
}
Status LogWritableBlock::Finalize() {
DCHECK(state_ == CLEAN || state_ == DIRTY || state_ == FINALIZED)
<< "Invalid state: " << state_;
if (state_ == FINALIZED) {
return Status::OK();
}
SCOPED_CLEANUP({
container_->FinalizeBlock(block_offset_, block_length_);
state_ = FINALIZED;
});
VLOG(3) << "Finalizing block " << id();
if (state_ == DIRTY &&
FLAGS_block_manager_preflush_control == "finalize") {
// We do not mark the container as read-only if FlushDataAsync() fails
// since the corresponding metadata has not yet been appended.
RETURN_NOT_OK(FlushDataAsync());
}
return Status::OK();
}
size_t LogWritableBlock::BytesAppended() const {
return block_length_;
}
WritableBlock::State LogWritableBlock::state() const {
return state_;
}
void LogWritableBlock::DoClose() {
if (state_ == CLOSED) return;
if (container_->metrics()) {
container_->metrics()->generic_metrics.blocks_open_writing->Decrement();
container_->metrics()->generic_metrics.total_bytes_written->IncrementBy(
block_length_);
container_->metrics()->generic_metrics.total_blocks_created->Increment();
}
// Finalize() was not called; this indicates we should
// finalize the block.
if (state_ == CLEAN || state_ == DIRTY) {
container_->FinalizeBlock(block_offset_, block_length_);
}
LogBlockRefPtr lb = container_->block_manager()->CreateAndAddLogBlock(
container_, block_id_, block_offset_, block_length_);
CHECK(lb);
container_->BlockCreated(lb);
state_ = CLOSED;
}
Status LogWritableBlock::AppendMetadata() {
BlockRecordPB record;
id().CopyToPB(record.mutable_block_id());
record.set_op_type(CREATE);
record.set_timestamp_us(GetCurrentTimeMicros());
record.set_offset(block_offset_);
record.set_length(block_length_);
return container_->AppendMetadata(record);
}
////////////////////////////////////////////////////////////
// LogReadableBlock
////////////////////////////////////////////////////////////
// A log-backed block that has been opened for reading.
//
// Refers to a LogBlock representing the block's persisted metadata.
class LogReadableBlock : public ReadableBlock {
public:
explicit LogReadableBlock(LogBlockRefPtr log_block);
virtual ~LogReadableBlock();
virtual Status Close() OVERRIDE;
virtual const BlockId& id() const OVERRIDE;
virtual BlockManager* block_manager() const OVERRIDE;
virtual Status Size(uint64_t* sz) const OVERRIDE;
virtual Status Read(uint64_t offset, Slice result) const OVERRIDE;
virtual Status ReadV(uint64_t offset, ArrayView<Slice> results) const OVERRIDE;
virtual size_t memory_footprint() const OVERRIDE;
private:
// A reference to this block's metadata.
LogBlockRefPtr log_block_;
// Whether or not this block has been closed. Close() is thread-safe, so
// this must be an atomic primitive.
AtomicBool closed_;
DISALLOW_COPY_AND_ASSIGN(LogReadableBlock);
};
LogReadableBlock::LogReadableBlock(LogBlockRefPtr log_block)
: log_block_(std::move(log_block)),
closed_(false) {
if (log_block_->container()->metrics()) {
log_block_->container()->metrics()->generic_metrics.blocks_open_reading->Increment();
log_block_->container()->metrics()->generic_metrics.total_readable_blocks->Increment();
}
}
LogReadableBlock::~LogReadableBlock() {
WARN_NOT_OK(Close(), Substitute("Failed to close block $0",
id().ToString()));
}
Status LogReadableBlock::Close() {
if (closed_.CompareAndSet(false, true)) {
if (log_block_->container()->metrics()) {
log_block_->container()->metrics()->generic_metrics.blocks_open_reading->Decrement();
}
log_block_.reset();
}
return Status::OK();
}
BlockManager* LogReadableBlock::block_manager() const {
return log_block_->container()->block_manager();
}
const BlockId& LogReadableBlock::id() const {
return log_block_->block_id();
}
Status LogReadableBlock::Size(uint64_t* sz) const {
DCHECK(!closed_.Load());
*sz = log_block_->length();
return Status::OK();
}
Status LogReadableBlock::Read(uint64_t offset, Slice result) const {
return ReadV(offset, ArrayView<Slice>(&result, 1));
}
Status LogReadableBlock::ReadV(uint64_t offset, ArrayView<Slice> results) const {
DCHECK(!closed_.Load());
size_t read_length = accumulate(results.begin(), results.end(), static_cast<size_t>(0),
[&](int sum, const Slice& curr) {
return sum + curr.size();
});
uint64_t read_offset = log_block_->offset() + offset;
if (log_block_->length() < offset + read_length) {
return Status::IOError("Out-of-bounds read",
Substitute("read of [$0-$1) in block [$2-$3)",
read_offset,
read_offset + read_length,
log_block_->offset(),
log_block_->offset() + log_block_->length()));
}
MicrosecondsInt64 start_time = GetMonoTimeMicros();
RETURN_NOT_OK(log_block_->container()->ReadVData(read_offset, results));
MicrosecondsInt64 end_time = GetMonoTimeMicros();
int64_t dur = end_time - start_time;
TRACE_COUNTER_INCREMENT("lbm_read_time_us", dur);
const char* counter = BUCKETED_COUNTER_NAME("lbm_reads", dur);
TRACE_COUNTER_INCREMENT(counter, 1);
if (log_block_->container()->metrics()) {
log_block_->container()->metrics()->generic_metrics.total_bytes_read->IncrementBy(read_length);
}
return Status::OK();
}
size_t LogReadableBlock::memory_footprint() const {
return kudu_malloc_usable_size(this);
}
} // namespace internal
////////////////////////////////////////////////////////////
// LogBlockManager
////////////////////////////////////////////////////////////
static const uint64_t kBlockMapChunk = 1 << 4;
static const uint64_t kBlockMapMask = kBlockMapChunk - 1;
const char* LogBlockManager::kContainerMetadataFileSuffix = ".metadata";
const char* LogBlockManager::kContainerDataFileSuffix = ".data";
// These values were arrived at via experimentation. See commit 4923a74 for
// more details.
const map<int64_t, int64_t> LogBlockManager::kPerFsBlockSizeBlockLimits({
{ 1024, 673 },
{ 2048, 1353 },
{ 4096, 2721 }});
LogBlockManager::LogBlockManager(Env* env,
DataDirManager* dd_manager,
FsErrorManager* error_manager,
FileCache* file_cache,
BlockManagerOptions opts)
: env_(DCHECK_NOTNULL(env)),
dd_manager_(DCHECK_NOTNULL(dd_manager)),
error_manager_(DCHECK_NOTNULL(error_manager)),
opts_(std::move(opts)),
mem_tracker_(MemTracker::CreateTracker(-1,
"log_block_manager",
opts_.parent_mem_tracker)),
file_cache_(file_cache),
buggy_el6_kernel_(IsBuggyEl6Kernel(env->GetKernelRelease())),
next_block_id_(1) {
managed_block_shards_.resize(kBlockMapChunk);
for (auto& mb : managed_block_shards_) {
mb.lock = std::unique_ptr<simple_spinlock>(new simple_spinlock());
mb.blocks_by_block_id
= std::unique_ptr<BlockMap>(new BlockMap(10,
BlockMap::hasher(),
BlockMap::key_equal(),
BlockAllocator(mem_tracker_)));
mb.blocks_by_block_id->set_deleted_key(BlockId());
}
// HACK: when running in a test environment, we often instantiate many
// LogBlockManagers in the same process, eg corresponding to different
// tablet servers in a minicluster, or due to running many separate test
// cases of some CFile-related code. In that case, we need to make it more
// likely that the block IDs are not reused. So, instead of starting with
// block ID 1, we'll start with a random block ID. A collision is still
// possible, but exceedingly unlikely.
if (IsGTest()) {
Random r(GetRandomSeed32());
next_block_id_.Store(r.Next64());
}
if (opts_.metric_entity) {
metrics_.reset(new internal::LogBlockManagerMetrics(opts_.metric_entity));
}
}
LogBlockManager::~LogBlockManager() {
// Release all of the memory accounted by the blocks.
int64_t mem = 0;
for (const auto& mb : managed_block_shards_) {
for (const auto& entry : *mb.blocks_by_block_id) {
mem += kudu_malloc_usable_size(entry.second.get());
}
}
mem_tracker_->Release(mem);
// A LogBlock's destructor depends on its container, so all LogBlocks must be
// destroyed before their containers.
for (auto& mb : managed_block_shards_) {
mb.blocks_by_block_id->clear();
}
// Containers may have outstanding tasks running on data directories; wait
// for them to complete before destroying the containers.
dd_manager_->WaitOnClosures();
}
// Ensure that no load task failed without being handled.
//
// Currently only disk failures are handled. Reports from failed disks are
// unusable.
#define RETURN_ON_NON_DISK_FAILURE(d, s) \
do { \
if (PREDICT_FALSE(!(s).ok())) { \
if (!(s).IsDiskFailure()) { \
return s; \
} \
LOG(ERROR) << Substitute("Not using report from $0: $1", \
(d)->dir(), (s).ToString()); \
} \
} while (false)
Status LogBlockManager::Open(FsReport* report) {
// Establish (and log) block limits for each data directory using kernel,
// filesystem, and gflags information.
for (const auto& dd : dd_manager_->dirs()) {
boost::optional<int64_t> limit;
if (FLAGS_log_container_max_blocks == -1) {
// No limit, unless this is KUDU-1508.
// The log block manager requires hole punching and, of the ext
// filesystems, only ext4 supports it. Thus, if this is an ext
// filesystem, it's ext4 by definition.
if (buggy_el6_kernel_ && dd->fs_type() == FsType::EXT) {
uint64_t fs_block_size =
dd->instance()->metadata()->filesystem_block_size_bytes();
bool untested_block_size =
!ContainsKey(kPerFsBlockSizeBlockLimits, fs_block_size);
string msg = Substitute(
"Data dir $0 is on an ext4 filesystem vulnerable to KUDU-1508 "
"with $1block size $2", dd->dir(),
untested_block_size ? "untested " : "", fs_block_size);
if (untested_block_size) {
LOG(WARNING) << msg;
} else {
LOG(INFO) << msg;
}
limit = LookupBlockLimit(fs_block_size);
}
} else if (FLAGS_log_container_max_blocks > 0) {
// Use the provided limit.
limit = FLAGS_log_container_max_blocks;
}
if (limit) {
LOG(INFO) << Substitute(
"Limiting containers on data directory $0 to $1 blocks",
dd->dir(), *limit);
}
InsertOrDie(&block_limits_by_data_dir_, dd.get(), limit);
}
// Open containers in each data dirs.
vector<Status> statuses(dd_manager_->dirs().size());
vector<vector<unique_ptr<internal::LogBlockContainerLoadResult>>> container_results(
dd_manager_->dirs().size());
int i = -1;
for (const auto& dd : dd_manager_->dirs()) {
i++;
int uuid_idx;
CHECK(dd_manager_->FindUuidIndexByDir(dd.get(), &uuid_idx));
// TODO(awong): store Statuses for each directory in the directory manager
// so we can avoid these artificial Statuses.
if (dd_manager_->IsDirFailed(uuid_idx)) {
statuses[i] = Status::IOError("Data directory failed", "", EIO);
continue;
}
// Open the data dir asynchronously.
auto* dd_raw = dd.get();
auto* results = &container_results[i];
auto* s = &statuses[i];
dd->ExecClosure([this, dd_raw, results, s]() {
this->OpenDataDir(dd_raw, results, s);
});
}
// Wait for the opens to complete.
for (const auto& dd : dd_manager_->dirs()) {
dd->WaitOnClosures();
}
// Check load errors and merge each data dir's container load results, then do repair tasks.
vector<unique_ptr<internal::LogBlockContainerLoadResult>> dir_results(
dd_manager_->dirs().size());
for (int i = 0; i < dd_manager_->dirs().size(); ++i) {
const auto& s = statuses[i];
const auto& dd = dd_manager_->dirs()[i];
RETURN_ON_NON_DISK_FAILURE(dd, s);
// If open dir error, do not try to repair.
if (PREDICT_FALSE(!s.ok())) {
continue;
}
unique_ptr<internal::LogBlockContainerLoadResult> dir_result(
new internal::LogBlockContainerLoadResult());
dir_result->report.data_dirs.push_back(dd->dir());
bool do_repair = true;
for (const auto& container_result : container_results[i]) {
RETURN_ON_NON_DISK_FAILURE(dd, container_result->status);
if (PREDICT_FALSE(!s.ok())) {
// If open container error, do not try to repair.
do_repair = false;
break;
}
dir_result->report.MergeFrom(container_result->report);
dir_result->dead_containers.insert(
dir_result->dead_containers.end(),
container_result->dead_containers.begin(),
container_result->dead_containers.end());
container_result->dead_containers.clear();
dir_result->low_live_block_containers.insert(
container_result->low_live_block_containers.begin(),
container_result->low_live_block_containers.end());
container_result->low_live_block_containers.clear();
dir_result->need_repunching_blocks.insert(
dir_result->need_repunching_blocks.end(),
container_result->need_repunching_blocks.begin(),
container_result->need_repunching_blocks.end());
container_result->need_repunching_blocks.clear();
}
if (do_repair) {
dir_results[i] = std::move(dir_result);
auto* dd_raw = dd.get();
auto* dr = dir_results[i].get();
dd->ExecClosure([this, dd_raw, dr]() { this->RepairTask(dd_raw, dr); });
}
}
// Wait for the repair tasks to complete.
for (const auto& dd : dd_manager_->dirs()) {
dd->WaitOnClosures();
}
FsReport merged_report;
for (int i = 0; i < dd_manager_->dirs().size(); ++i) {
if (PREDICT_FALSE(!dir_results[i])) {
continue;
}
if (PREDICT_TRUE(dir_results[i]->status.ok())) {
merged_report.MergeFrom(dir_results[i]->report);
continue;
}
RETURN_ON_NON_DISK_FAILURE(dd_manager_->dirs()[i], dir_results[i]->status);
}
if (dd_manager_->GetFailedDirs().size() == dd_manager_->dirs().size()) {
return Status::IOError("All data dirs failed to open", "", EIO);
}
// Either return or log the report.
if (report) {
*report = std::move(merged_report);
} else {
RETURN_NOT_OK(merged_report.LogAndCheckForFatalErrors());
}
return Status::OK();
}
Status LogBlockManager::CreateBlock(const CreateBlockOptions& opts,
unique_ptr<WritableBlock>* block) {
CHECK(!opts_.read_only);
// Find a free container. If one cannot be found, create a new one.
//
// TODO(unknown): should we cap the number of outstanding containers and
// force callers to block if we've reached it?
LogBlockContainerRefPtr container;
RETURN_NOT_OK(GetOrCreateContainer(opts, &container));
// Generate a free block ID.
// We have to loop here because earlier versions used non-sequential block IDs,
// and thus we may have to "skip over" some block IDs that are claimed.
BlockId new_block_id;
do {
new_block_id.SetId(next_block_id_.Increment());
} while (!TryUseBlockId(new_block_id));
block->reset(new LogWritableBlock(container,
new_block_id,
container->next_block_offset()));
VLOG(3) << "Created block " << (*block)->id() << " in container "
<< container->ToString();
return Status::OK();
}
Status LogBlockManager::OpenBlock(const BlockId& block_id,
unique_ptr<ReadableBlock>* block) {
LogBlockRefPtr lb;
{
int index = block_id.id() & kBlockMapMask;
std::lock_guard<simple_spinlock> l(*managed_block_shards_[index].lock);
lb = FindPtrOrNull(*managed_block_shards_[index].blocks_by_block_id, block_id);
}
if (!lb) {
return Status::NotFound("Can't find block", block_id.ToString());
}
VLOG(3) << "Opened block " << block_id
<< " from container " << lb->container()->ToString();
block->reset(new internal::LogReadableBlock(std::move(lb)));
return Status::OK();
}
unique_ptr<BlockCreationTransaction> LogBlockManager::NewCreationTransaction() {
CHECK(!opts_.read_only);
return unique_ptr<internal::LogBlockCreationTransaction>(
new internal::LogBlockCreationTransaction());
}
shared_ptr<BlockDeletionTransaction> LogBlockManager::NewDeletionTransaction() {
CHECK(!opts_.read_only);
return std::make_shared<internal::LogBlockDeletionTransaction>(this);
}
Status LogBlockManager::GetAllBlockIds(vector<BlockId>* block_ids) {
block_ids->clear();
for (const auto& mb : managed_block_shards_) {
std::lock_guard<simple_spinlock> l(*mb.lock);
AppendKeysFromMap(*mb.blocks_by_block_id, block_ids);
block_ids->insert(block_ids->end(), mb.open_block_ids.begin(), mb.open_block_ids.end());
}
return Status::OK();
}
void LogBlockManager::NotifyBlockId(BlockId block_id) {
next_block_id_.StoreMax(block_id.id() + 1);
}
void LogBlockManager::AddNewContainerUnlocked(const LogBlockContainerRefPtr& container) {
DCHECK(lock_.is_locked());
InsertOrDie(&all_containers_by_name_, container->ToString(), container);
if (metrics()) {
metrics()->containers->Increment();
if (container->full()) {
metrics()->full_containers->Increment();
}
}
}
void LogBlockManager::RemoveDeadContainerUnlocked(const string& container_name) {
DCHECK(lock_.is_locked());
LogBlockContainerRefPtr to_delete(EraseKeyReturnValuePtr(
&all_containers_by_name_, container_name));
CHECK(to_delete);
CHECK(to_delete->full())
<< Substitute("Container $0 is not full", container_name);
CHECK(to_delete->dead())
<< Substitute("Container $0 is not dead", container_name);
if (metrics()) {
metrics()->containers->Decrement();
metrics()->full_containers->Decrement();
}
}
void LogBlockManager::RemoveDeadContainer(const string& container_name) {
// Remove the container from memory.
{
std::lock_guard<simple_spinlock> l(lock_);
RemoveDeadContainerUnlocked(container_name);
}
// Update the metrics if necessary.
if (metrics()) {
metrics()->dead_containers_deleted->Increment();
}
}
Status LogBlockManager::GetOrCreateContainer(const CreateBlockOptions& opts,
LogBlockContainerRefPtr* container) {
Dir* dir;
RETURN_NOT_OK_EVAL(dd_manager_->GetDirAddIfNecessary(opts, &dir),
error_manager_->RunErrorNotificationCb(ErrorHandlerType::NO_AVAILABLE_DISKS, opts.tablet_id));
{
std::lock_guard<simple_spinlock> l(lock_);
auto& d = available_containers_by_data_dir_[DCHECK_NOTNULL(dir)];
if (!d.empty()) {
*container = d.front();
d.pop_front();
return Status::OK();
}
}
// All containers are in use; create a new one.
LogBlockContainerRefPtr new_container;
Status s = LogBlockContainer::Create(this, dir, &new_container);
// We could create a container in a different directory, but there's
// currently no point in doing so. On disk failure, the tablet specified by
// 'opts' will be shut down, so the returned container would not be used.
HANDLE_DISK_FAILURE(s, error_manager_->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR, dir));
RETURN_NOT_OK_PREPEND(s, "Could not create new log block container at " + dir->dir());
{
std::lock_guard<simple_spinlock> l(lock_);
dirty_dirs_.insert(dir->dir());
AddNewContainerUnlocked(new_container);
}
*container = std::move(new_container);
return Status::OK();
}
void LogBlockManager::MakeContainerAvailable(LogBlockContainerRefPtr container) {
std::lock_guard<simple_spinlock> l(lock_);
MakeContainerAvailableUnlocked(std::move(container));
}
void LogBlockManager::MakeContainerAvailableUnlocked(LogBlockContainerRefPtr container) {
DCHECK(lock_.is_locked());
if (container->full() || container->read_only()) {
return;
}
VLOG(3) << Substitute("container $0 being made available", container->ToString());
available_containers_by_data_dir_[container->data_dir()].push_front(std::move(container));
}
Status LogBlockManager::SyncContainer(const LogBlockContainer& container) {
Status s;
bool to_sync = false;
{
std::lock_guard<simple_spinlock> l(lock_);
to_sync = dirty_dirs_.erase(container.data_dir()->dir());
}
if (to_sync && FLAGS_enable_data_block_fsync) {
if (metrics_) metrics_->generic_metrics.total_disk_sync->Increment();
s = env_->SyncDir(container.data_dir()->dir());
// If SyncDir fails, the container directory must be restored to
// dirty_dirs_. Otherwise a future successful LogWritableBlock::Close()
// on this container won't call SyncDir again, and the container might
// be lost on crash.
//
// In the worst case (another block synced this container as we did),
// we'll sync it again needlessly.
if (!s.ok()) {
container.HandleError(s);
std::lock_guard<simple_spinlock> l(lock_);
dirty_dirs_.insert(container.data_dir()->dir());
}
}
return s;
}
bool LogBlockManager::TryUseBlockId(const BlockId& block_id) {
if (block_id.IsNull()) {
return false;
}
int index = block_id.id() & kBlockMapMask;
std::lock_guard<simple_spinlock> l(*managed_block_shards_[index].lock);
if (ContainsKey(*managed_block_shards_[index].blocks_by_block_id, block_id)) {
return false;
}
return InsertIfNotPresent(&managed_block_shards_[index].open_block_ids, block_id);
}
LogBlockRefPtr LogBlockManager::CreateAndAddLogBlock(
LogBlockContainerRefPtr container,
const BlockId& block_id,
int64_t offset,
int64_t length) {
LogBlockRefPtr lb(new LogBlock(std::move(container), block_id, offset, length));
mem_tracker_->Consume(kudu_malloc_usable_size(lb.get()));
if (AddLogBlock(lb)) {
return lb;
}
return nullptr;
}
bool LogBlockManager::AddLogBlock(LogBlockRefPtr lb) {
// InsertIfNotPresent doesn't use move semantics, so instead we just
// insert an empty scoped_refptr and assign into it down below rather
// than using the utility function.
int index = lb->block_id().id() & kBlockMapMask;
std::lock_guard<simple_spinlock> l(*managed_block_shards_[index].lock);
auto& blocks_by_block_id = *managed_block_shards_[index].blocks_by_block_id;
LogBlockRefPtr* entry_ptr = &blocks_by_block_id[lb->block_id()];
if (*entry_ptr) {
// Already have an entry for this block ID.
return false;
}
VLOG(2) << Substitute("Added block: id $0, offset $1, length $2",
lb->block_id().ToString(), lb->offset(), lb->length());
// There may already be an entry in open_block_ids_arr_ (e.g. we just finished
// writing out a block).
managed_block_shards_[index].open_block_ids.erase(lb->block_id());
if (metrics()) {
metrics()->blocks_under_management->Increment();
metrics()->bytes_under_management->IncrementBy(lb->length());
}
*entry_ptr = std::move(lb);
return true;
}
Status LogBlockManager::RemoveLogBlocks(vector<BlockId> block_ids,
vector<LogBlockRefPtr>* log_blocks,
vector<BlockId>* deleted) {
Status first_failure;
vector<LogBlockRefPtr> lbs;
int64_t malloc_space = 0, blocks_length = 0;
for (const auto& block_id : block_ids) {
LogBlockRefPtr lb;
Status s = RemoveLogBlock(block_id, &lb);
// If we get NotFound, then the block was already deleted.
if (!s.ok() && !s.IsNotFound()) {
if (first_failure.ok()) first_failure = s;
} else if (s.ok()) {
malloc_space += kudu_malloc_usable_size(lb.get());
blocks_length += lb->length();
lbs.emplace_back(std::move(lb));
} else {
deleted->emplace_back(block_id);
}
}
// Update various metrics.
mem_tracker_->Release(malloc_space);
if (metrics()) {
metrics()->blocks_under_management->DecrementBy(lbs.size());
metrics()->bytes_under_management->DecrementBy(blocks_length);
}
for (auto& lb : lbs) {
VLOG(3) << "Deleting block " << lb->block_id();
lb->container()->BlockDeleted(lb);
// Record the on-disk deletion.
//
// TODO(unknown): what if this fails? Should we restore the in-memory block?
BlockRecordPB record;
lb->block_id().CopyToPB(record.mutable_block_id());
record.set_op_type(DELETE);
record.set_timestamp_us(GetCurrentTimeMicros());
Status s = lb->container()->AppendMetadata(record);
// We don't bother fsyncing the metadata append for deletes in order to avoid
// the disk overhead. Even if we did fsync it, we'd still need to account for
// garbage at startup time (in the event that we crashed just before the
// fsync).
//
// TODO(KUDU-829): Implement GC of orphaned blocks.
if (!s.ok()) {
if (first_failure.ok()) {
first_failure = s.CloneAndPrepend(
"Unable to append deletion record to block metadata");
}
} else {
deleted->emplace_back(lb->block_id());
log_blocks->emplace_back(std::move(lb));
}
}
return first_failure;
}
Status LogBlockManager::RemoveLogBlock(const BlockId& block_id,
LogBlockRefPtr* lb) {
int index = block_id.id() & kBlockMapMask;
std::lock_guard<simple_spinlock> l(*managed_block_shards_[index].lock);
auto& blocks_by_block_id = managed_block_shards_[index].blocks_by_block_id;
auto it = blocks_by_block_id->find(block_id);
if (it == blocks_by_block_id->end()) {
return Status::NotFound("Can't find block", block_id.ToString());
}
LogBlockContainer* container = it->second->container();
HANDLE_DISK_FAILURE(container->read_only_status(),
error_manager_->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR, container->data_dir()));
// Return early if deleting a block in a failed directory.
set<int> failed_dirs = dd_manager_->GetFailedDirs();
if (PREDICT_FALSE(!failed_dirs.empty())) {
int uuid_idx;
CHECK(dd_manager_->FindUuidIndexByDir(container->data_dir(), &uuid_idx));
if (ContainsKey(failed_dirs, uuid_idx)) {
LOG_EVERY_N(INFO, 10) << Substitute("Block $0 is in a failed directory; not deleting",
block_id.ToString());
return Status::IOError("Block is in a failed directory");
}
}
*lb = std::move(it->second);
blocks_by_block_id->erase(it);
VLOG(2) << Substitute("Removed block: id $0, offset $1, length $2",
(*lb)->block_id().ToString(), (*lb)->offset(), (*lb)->length());
return Status::OK();
}
void LogBlockManager::OpenDataDir(
Dir* dir,
vector<unique_ptr<internal::LogBlockContainerLoadResult>>* results,
Status* result_status) {
// Find all containers and open them.
unordered_set<string> containers_seen;
vector<string> children;
Status s = env_->GetChildren(dir->dir(), &children);
if (!s.ok()) {
HANDLE_DISK_FAILURE(s, error_manager_->RunErrorNotificationCb(
ErrorHandlerType::DISK_ERROR, dir));
*result_status = s.CloneAndPrepend(Substitute(
"Could not list children of $0", dir->dir()));
return;
}
for (const string& child : children) {
string container_name;
if (!TryStripSuffixString(
child, LogBlockManager::kContainerDataFileSuffix, &container_name) &&
!TryStripSuffixString(
child, LogBlockManager::kContainerMetadataFileSuffix, &container_name)) {
continue;
}
if (!InsertIfNotPresent(&containers_seen, container_name)) {
continue;
}
// Add a new result for the container.
results->emplace_back(new internal::LogBlockContainerLoadResult());
LogBlockContainerRefPtr container;
s = LogBlockContainer::Open(
this, dir, &results->back()->report, container_name, &container);
if (!s.ok()) {
if (s.IsAborted()) {
// Skip the container. Open() added a record of it to 'results->back()->report' for us.
continue;
}
if (opts_.read_only && s.IsNotFound()) {
// Skip the container while the operation is read-only and the files are away,
// especially for the kudu cli tool.
continue;
}
*result_status = s.CloneAndPrepend(Substitute(
"Could not open container $0", container_name));
return;
}
// Load the container's records asynchronously.
auto* r = results->back().get();
dir->ExecClosure([this, dir, container, r]() {
this->LoadContainer(dir, container, r);
});
}
}
void LogBlockManager::LoadContainer(Dir* dir,
LogBlockContainerRefPtr container,
internal::LogBlockContainerLoadResult* result) {
// Process the records, building a container-local map for live blocks and
// a list of dead blocks.
//
// It's important that we don't try to add these blocks to the global map
// incrementally as we see each record, since it's possible that one container
// has a "CREATE <b>" while another has a "CREATE <b> ; DELETE <b>" pair.
// If we processed those two containers in this order, then upon processing
// the second container, we'd think there was a duplicate block. Building
// the container-local map first ensures that we discount deleted blocks
// before checking for duplicate IDs.
//
// NOTE: Since KUDU-1538, we allocate sequential block IDs, which makes reuse
// exceedingly unlikely. However, we might have old data which still exhibits
// the above issue.
UntrackedBlockMap live_blocks;
BlockRecordMap live_block_records;
vector<LogBlockRefPtr> dead_blocks;
uint64_t max_block_id = 0;
Status s = container->ProcessRecords(&result->report,
&live_blocks,
&live_block_records,
&dead_blocks,
&max_block_id);
if (!s.ok()) {
result->status = s.CloneAndPrepend(Substitute(
"Could not process records in container $0", container->ToString()));
return;
}
// With deleted blocks out of the way, check for misaligned blocks.
//
// We could also enforce that the record's offset is aligned with the
// underlying filesystem's block size, an invariant maintained by the log
// block manager. However, due to KUDU-1793, that invariant may have been
// broken, so we'll note but otherwise allow it.
for (const auto& e : live_blocks) {
if (PREDICT_FALSE(e.second->offset() %
container->instance()->filesystem_block_size_bytes() != 0)) {
result->report.misaligned_block_check->entries.emplace_back(
container->ToString(), e.first);
}
}
if (container->full()) {
// Full containers without any live blocks can be deleted outright.
//
// TODO(adar): this should be reported as an inconsistency once dead
// container deletion is also done in real time. Until then, it would be
// confusing to report it as such since it'll be a natural event at startup.
if (container->live_blocks() == 0) {
DCHECK(live_blocks.empty());
result->dead_containers.emplace_back(container);
} else if (static_cast<double>(container->live_blocks()) /
container->total_blocks() <= FLAGS_log_container_live_metadata_before_compact_ratio) {
// Metadata files of containers with very few live blocks will be compacted.
//
// TODO(adar): this should be reported as an inconsistency once
// container metadata compaction is also done in realtime. Until then,
// it would be confusing to report it as such since it'll be a natural
// event at startup.
vector<BlockRecordPB> records(live_block_records.size());
int i = 0;
for (auto& e : live_block_records) {
records[i].Swap(&e.second);
i++;
}
// Sort the records such that their ordering reflects the ordering in
// the pre-compacted metadata file.
//
// This is preferred to storing the records in an order-preserving
// container (such as std::map) because while records are temporarily
// retained for every container, only some containers will actually
// undergo metadata compaction.
std::sort(records.begin(), records.end(),
[](const BlockRecordPB& a, const BlockRecordPB& b) {
// Sort by timestamp.
if (a.timestamp_us() != b.timestamp_us()) {
return a.timestamp_us() < b.timestamp_us();
}
// If the timestamps match, sort by offset.
//
// If the offsets also match (i.e. both blocks are of zero length),
// it doesn't matter which of the two records comes first.
return a.offset() < b.offset();
});
result->low_live_block_containers[container->ToString()] = std::move(records);
}
// Having processed the block records, let's check whether any full
// containers have any extra space (left behind after a crash or from an
// older version of Kudu).
//
// Filesystems are unpredictable beasts and may misreport the amount of
// space allocated to a file in various interesting ways. Some examples:
// - XFS's speculative preallocation feature may artificially enlarge the
// container's data file without updating its file size. This makes the
// file size untrustworthy for the purposes of measuring allocated space.
// See KUDU-1856 for more details.
// - On el6.6/ext4 a container data file that consumed ~32K according to
// its extent tree was actually reported as consuming an additional fs
// block (2k) of disk space. A similar container data file (generated
// via the same workload) on Ubuntu 16.04/ext4 did not exhibit this.
// The suspicion is that older versions of ext4 include interior nodes
// of the extent tree when reporting file block usage.
//
// To deal with these issues, our extra space cleanup code (deleted block
// repunching and container truncation) is gated on an "actual disk space
// consumed" heuristic. To prevent unnecessary triggering of the
// heuristic, we allow for some slop in our size measurements. The exact
// amount of slop is configurable via
// log_container_excess_space_before_cleanup_fraction.
//
// Too little slop and we'll do unnecessary work at startup. Too much and
// more unused space may go unreclaimed.
string data_filename = StrCat(container->ToString(), kContainerDataFileSuffix);
uint64_t reported_size;
s = env_->GetFileSizeOnDisk(data_filename, &reported_size);
if (!s.ok()) {
HANDLE_DISK_FAILURE(s, error_manager_->RunErrorNotificationCb(
ErrorHandlerType::DISK_ERROR, dir));
result->status = s.CloneAndPrepend(Substitute(
"Could not get on-disk file size of container $0", container->ToString()));
return;
}
int64_t cleanup_threshold_size = container->live_bytes_aligned() *
(1 + FLAGS_log_container_excess_space_before_cleanup_fraction);
if (reported_size > cleanup_threshold_size) {
result->report.full_container_space_check->entries.emplace_back(
container->ToString(), reported_size - container->live_bytes_aligned());
// If the container is to be deleted outright, don't bother repunching
// its blocks. The report entry remains, however, so it's clear that
// there was a space discrepancy.
if (container->live_blocks()) {
result->need_repunching_blocks.insert(result->need_repunching_blocks.end(),
dead_blocks.begin(), dead_blocks.end());
}
}
result->report.stats.lbm_full_container_count++;
}
result->report.stats.live_block_bytes += container->live_bytes();
result->report.stats.live_block_bytes_aligned += container->live_bytes_aligned();
result->report.stats.live_block_count += container->live_blocks();
result->report.stats.lbm_container_count++;
next_block_id_.StoreMax(max_block_id + 1);
int64_t mem_usage = 0;
for (UntrackedBlockMap::value_type& e : live_blocks) {
int block_mem = kudu_malloc_usable_size(e.second.get());
if (!AddLogBlock(std::move(e.second))) {
// TODO(adar): track as an inconsistency?
LOG(FATAL) << "Found duplicate CREATE record for block " << e.first
<< " which already is alive from another container when "
<< " processing container " << container->ToString();
}
mem_usage += block_mem;
}
mem_tracker_->Consume(mem_usage);
int64_t container_count = 0;
{
std::lock_guard<simple_spinlock> l(lock_);
AddNewContainerUnlocked(container);
MakeContainerAvailableUnlocked(std::move(container));
container_count = all_containers_by_name_.size();
}
// Log every 200 number of log block containers
if (container_count % 200 == 0) {
LOG(INFO) << Substitute("Opened $0 log block containers", container_count);
}
}
void LogBlockManager::RepairTask(Dir* dir, internal::LogBlockContainerLoadResult* result) {
result->status = Repair(dir,
&result->report,
std::move(result->need_repunching_blocks),
std::move(result->dead_containers),
std::move(result->low_live_block_containers));
}
#define RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(status_expr, msg) do { \
Status s_ = (status_expr); \
s_ = s_.CloneAndPrepend(msg); \
RETURN_NOT_OK_HANDLE_DISK_FAILURE(s_, \
error_manager_->RunErrorNotificationCb(ErrorHandlerType::DISK_ERROR, dir)); \
} while (0)
#define WARN_NOT_OK_LBM_DISK_FAILURE(status_expr, msg) do { \
Status s_ = (status_expr); \
HANDLE_DISK_FAILURE(s_, error_manager_->RunErrorNotificationCb( \
ErrorHandlerType::DISK_ERROR, dir)); \
WARN_NOT_OK(s_, msg); \
} while (0)
Status LogBlockManager::Repair(
Dir* dir,
FsReport* report,
vector<LogBlockRefPtr> need_repunching,
vector<LogBlockContainerRefPtr> dead_containers,
unordered_map<string, vector<BlockRecordPB>> low_live_block_containers) {
if (opts_.read_only) {
LOG(INFO) << "Read-only block manager, skipping repair";
return Status::OK();
}
if (report->HasFatalErrors()) {
LOG(WARNING) << "Found fatal and irreparable errors, skipping repair";
return Status::OK();
}
// From here on out we're committed to repairing.
// Fetch all the containers we're going to need.
unordered_map<string, LogBlockContainerRefPtr> containers_by_name;
{
std::lock_guard<simple_spinlock> l(lock_);
// Remove all of the dead containers from the block manager. They will be
// deleted from disk shortly thereafter, outside of the lock.
for (const auto& d : dead_containers) {
CHECK(d->TrySetDead());
RemoveDeadContainerUnlocked(d->ToString());
// This must be true if clearing dead_containers below is to delete the
// container files.
DCHECK(d->HasOneRef());
}
// Fetch all the containers we're going to need.
if (report->partial_record_check) {
for (const auto& pr : report->partial_record_check->entries) {
LogBlockContainerRefPtr c = FindPtrOrNull(all_containers_by_name_,
pr.container);
if (c) {
containers_by_name[pr.container] = c;
}
}
}
if (report->full_container_space_check) {
for (const auto& fcp : report->full_container_space_check->entries) {
LogBlockContainerRefPtr c = FindPtrOrNull(all_containers_by_name_,
fcp.container);
if (c) {
containers_by_name[fcp.container] = c;
}
}
}
for (const auto& e : low_live_block_containers) {
LogBlockContainerRefPtr c = FindPtrOrNull(all_containers_by_name_,
e.first);
if (c) {
containers_by_name[e.first] = c;
}
}
}
// Clearing this vector drops the last references to the containers within,
// triggering their deletion.
dead_containers.clear();
// Truncate partial metadata records.
//
// This is a fatal inconsistency; if the repair fails, we cannot proceed.
if (report->partial_record_check) {
for (auto& pr : report->partial_record_check->entries) {
unique_ptr<RWFile> file;
RWFileOptions opts;
opts.mode = Env::MUST_EXIST;
LogBlockContainerRefPtr container = FindPtrOrNull(containers_by_name, pr.container);
if (!container) {
// The container was deleted outright.
pr.repaired = true;
continue;
}
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(
env_->NewRWFile(opts,
StrCat(pr.container, kContainerMetadataFileSuffix),
&file),
"could not reopen container to truncate partial metadata record");
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(file->Truncate(pr.offset),
"could not truncate partial metadata record");
// Technically we've "repaired" the inconsistency if the truncation
// succeeded, even if the following logic fails.
pr.repaired = true;
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(file->Close(),
"could not close container after truncating partial metadata record");
// Reopen the PB writer so that it will refresh its metadata about the
// underlying file and resume appending to the new end of the file.
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(container->ReopenMetadataWriter(),
"could not reopen container metadata file");
}
}
// Delete any incomplete container files.
//
// This is a non-fatal inconsistency; we can just as easily ignore the
// leftover container files.
if (report->incomplete_container_check) {
for (auto& ic : report->incomplete_container_check->entries) {
Status s = env_->DeleteFile(
StrCat(ic.container, kContainerMetadataFileSuffix));
if (!s.ok() && !s.IsNotFound()) {
WARN_NOT_OK_LBM_DISK_FAILURE(s, "could not delete incomplete container metadata file");
}
s = env_->DeleteFile(StrCat(ic.container, kContainerDataFileSuffix));
if (!s.ok() && !s.IsNotFound()) {
WARN_NOT_OK_LBM_DISK_FAILURE(s, "could not delete incomplete container data file");
}
ic.repaired = true;
}
}
// Truncate any excess preallocated space in full containers.
//
// This is a non-fatal inconsistency; we can just as easily ignore the extra
// disk space consumption.
if (report->full_container_space_check) {
for (auto& fcp : report->full_container_space_check->entries) {
LogBlockContainerRefPtr container = FindPtrOrNull(containers_by_name, fcp.container);
if (!container) {
// The container was deleted outright.
fcp.repaired = true;
continue;
}
Status s = container->TruncateDataToNextBlockOffset();
if (s.ok()) {
fcp.repaired = true;
}
WARN_NOT_OK(s, "could not truncate excess preallocated space");
}
}
// Repunch all requested holes. Any excess space reclaimed was already
// tracked by LBMFullContainerSpaceCheck.
//
// Register deletions to a single BlockDeletionTransaction. So, the repunched
// holes belonging to the same container can be coalesced.
shared_ptr<LogBlockDeletionTransaction> transaction =
std::make_shared<LogBlockDeletionTransaction>(this);
for (const auto& b : need_repunching) {
b->RegisterDeletion(transaction);
transaction->AddBlock(b);
}
// Clearing this vector drops the last references to the LogBlocks within,
// triggering the repunching operations.
need_repunching.clear();
// "Compact" metadata files with few live blocks by rewriting them with only
// the live block records.
int64_t metadata_files_compacted = 0;
int64_t metadata_bytes_delta = 0;
for (const auto& e : low_live_block_containers) {
LogBlockContainerRefPtr container = FindPtrOrNull(containers_by_name, e.first);
if (!container) {
// The container was deleted outright.
continue;
}
// Rewrite this metadata file. Failures are non-fatal.
int64_t file_bytes_delta;
const auto& meta_path = StrCat(e.first, kContainerMetadataFileSuffix);
Status s = RewriteMetadataFile(*(container.get()), e.second, &file_bytes_delta);
if (!s.ok()) {
WARN_NOT_OK(s, "could not rewrite metadata file");
continue;
}
// However, we're hosed if we can't open the new metadata file.
RETURN_NOT_OK_PREPEND(container->ReopenMetadataWriter(),
"could not reopen new metadata file");
metadata_files_compacted++;
metadata_bytes_delta += file_bytes_delta;
VLOG(1) << "Compacted metadata file " << meta_path
<< " (saved " << file_bytes_delta << " bytes)";
}
// The data directory can be synchronized once for all of the new metadata files.
//
// Non-disk failures are fatal: if a new metadata file doesn't durably exist
// in the data directory, it would be unsafe to append new block records to
// it. This is because after a crash the old metadata file may appear
// instead, and that file lacks the newly appended block records.
//
// TODO(awong): The below will only be true with persistent disk states.
// Disk failures do not suffer from this issue because, on the next startup,
// the entire directory will not be used.
if (metadata_files_compacted > 0) {
Status s = env_->SyncDir(dir->dir());
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(s, "Could not sync data directory");
LOG(INFO) << Substitute("Compacted $0 metadata files ($1 metadata bytes)",
metadata_files_compacted, metadata_bytes_delta);
}
return Status::OK();
}
Status LogBlockManager::RewriteMetadataFile(const LogBlockContainer& container,
const vector<BlockRecordPB>& records,
int64_t* file_bytes_delta) {
uint64_t old_metadata_size;
const string metadata_file_name = StrCat(container.ToString(), kContainerMetadataFileSuffix);
// Get the container's data directory's UUID for error handling.
const string dir = container.data_dir()->dir();
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(env_->GetFileSize(metadata_file_name, &old_metadata_size),
"could not get size of old metadata file");
// Create a new container metadata file with only the live block records.
//
// By using a temporary file and renaming it over the original file at the
// end, we ensure that we can recover from a failure at any point. Any
// temporary files left behind are cleaned up by the FsManager at startup.
string tmpl = metadata_file_name + kTmpInfix + ".XXXXXX";
unique_ptr<RWFile> tmp_file;
string tmp_file_name;
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(env_->NewTempRWFile(RWFileOptions(), tmpl,
&tmp_file_name, &tmp_file),
"could not create temporary metadata file");
auto tmp_deleter = MakeScopedCleanup([&]() {
WARN_NOT_OK(env_->DeleteFile(tmp_file_name),
"Could not delete file " + tmp_file_name);
});
WritablePBContainerFile pb_file(std::move(tmp_file));
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(pb_file.CreateNew(BlockRecordPB()),
"could not initialize temporary metadata file");
for (const auto& r : records) {
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(pb_file.Append(r),
"could not append to temporary metadata file");
}
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(pb_file.Sync(),
"could not sync temporary metadata file");
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(pb_file.Close(),
"could not close temporary metadata file");
uint64_t new_metadata_size;
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(env_->GetFileSize(tmp_file_name, &new_metadata_size),
"could not get file size of temporary metadata file");
RETURN_NOT_OK_LBM_DISK_FAILURE_PREPEND(env_->RenameFile(tmp_file_name, metadata_file_name),
"could not rename temporary metadata file");
if (PREDICT_TRUE(file_cache_)) {
// Evict the old path from the file cache, so that when we re-open the new
// metadata file for write, we don't accidentally get a cache hit on the
// old file descriptor pointing to the now-deleted old version.
file_cache_->Invalidate(metadata_file_name);
}
tmp_deleter.cancel();
*file_bytes_delta = (static_cast<int64_t>(old_metadata_size) - new_metadata_size);
return Status::OK();
}
string LogBlockManager::ContainerPathForTests(internal::LogBlockContainer* container) {
return container->ToString();
}
bool LogBlockManager::IsBuggyEl6Kernel(const string& kernel_release) {
autodigit_less lt;
// Only el6 is buggy.
if (kernel_release.find("el6") == string::npos) return false;
// Kernels in the 6.8 update stream (2.6.32-642.a.b) are fixed
// for a >= 15.
//
// https://rhn.redhat.com/errata/RHSA-2017-0307.html
if (MatchPattern(kernel_release, "2.6.32-642.*.el6.*") &&
lt("2.6.32-642.15.0", kernel_release)) {
return false;
}
// If the kernel older is than 2.6.32-674 (el6.9), it's buggy.
return lt(kernel_release, "2.6.32-674");
}
int64_t LogBlockManager::LookupBlockLimit(int64_t fs_block_size) {
const int64_t* limit = FindFloorOrNull(kPerFsBlockSizeBlockLimits,
fs_block_size);
if (limit) {
return *limit;
}
// Block size must have been less than the very first key. Return the
// first recorded entry and hope for the best.
return kPerFsBlockSizeBlockLimits.begin()->second;
}
} // namespace fs
} // namespace kudu