|
|
|
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
|
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
|
|
//
|
|
|
|
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
#include <stddef.h>
|
|
|
|
#include <stdint.h>
|
|
|
|
#include <string>
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
#include "db/pinned_iterators_manager.h"
|
|
|
|
#include "port/malloc.h"
|
|
|
|
#include "rocksdb/iterator.h"
|
|
|
|
#include "rocksdb/options.h"
|
|
|
|
#include "rocksdb/statistics.h"
|
|
|
|
#include "rocksdb/table.h"
|
|
|
|
#include "table/block_based/block_prefix_index.h"
|
|
|
|
#include "table/block_based/data_block_hash_index.h"
|
|
|
|
#include "table/format.h"
|
|
|
|
#include "table/internal_iterator.h"
|
|
|
|
#include "test_util/sync_point.h"
|
|
|
|
#include "util/random.h"
|
|
|
|
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
|
|
|
|
struct BlockContents;
|
|
|
|
class Comparator;
|
|
|
|
template <class TValue>
|
|
|
|
class BlockIter;
|
|
|
|
class DataBlockIter;
|
|
|
|
class IndexBlockIter;
|
|
|
|
class MetaBlockIter;
|
|
|
|
class BlockPrefixIndex;
|
|
|
|
|
|
|
|
// BlockReadAmpBitmap is a bitmap that map the ROCKSDB_NAMESPACE::Block data
|
|
|
|
// bytes to a bitmap with ratio bytes_per_bit. Whenever we access a range of
|
|
|
|
// bytes in the Block we update the bitmap and increment
|
|
|
|
// READ_AMP_ESTIMATE_USEFUL_BYTES.
|
|
|
|
class BlockReadAmpBitmap {
|
|
|
|
public:
|
|
|
|
explicit BlockReadAmpBitmap(size_t block_size, size_t bytes_per_bit,
|
|
|
|
Statistics* statistics)
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
: bitmap_(nullptr),
|
|
|
|
bytes_per_bit_pow_(0),
|
|
|
|
statistics_(statistics),
|
|
|
|
rnd_(Random::GetTLSInstance()->Uniform(
|
|
|
|
static_cast<int>(bytes_per_bit))) {
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
TEST_SYNC_POINT_CALLBACK("BlockReadAmpBitmap:rnd", &rnd_);
|
|
|
|
assert(block_size > 0 && bytes_per_bit > 0);
|
|
|
|
|
|
|
|
// convert bytes_per_bit to be a power of 2
|
|
|
|
while (bytes_per_bit >>= 1) {
|
|
|
|
bytes_per_bit_pow_++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// num_bits_needed = ceil(block_size / bytes_per_bit)
|
|
|
|
size_t num_bits_needed = ((block_size - 1) >> bytes_per_bit_pow_) + 1;
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
assert(num_bits_needed > 0);
|
|
|
|
|
|
|
|
// bitmap_size = ceil(num_bits_needed / kBitsPerEntry)
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
size_t bitmap_size = (num_bits_needed - 1) / kBitsPerEntry + 1;
|
|
|
|
|
|
|
|
// Create bitmap and set all the bits to 0
|
|
|
|
bitmap_ = new std::atomic<uint32_t>[bitmap_size]();
|
|
|
|
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
RecordTick(GetStatistics(), READ_AMP_TOTAL_READ_BYTES, block_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
~BlockReadAmpBitmap() { delete[] bitmap_; }
|
|
|
|
|
|
|
|
void Mark(uint32_t start_offset, uint32_t end_offset) {
|
|
|
|
assert(end_offset >= start_offset);
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
// Index of first bit in mask
|
|
|
|
uint32_t start_bit =
|
|
|
|
(start_offset + (1 << bytes_per_bit_pow_) - rnd_ - 1) >>
|
|
|
|
bytes_per_bit_pow_;
|
|
|
|
// Index of last bit in mask + 1
|
|
|
|
uint32_t exclusive_end_bit =
|
|
|
|
(end_offset + (1 << bytes_per_bit_pow_) - rnd_) >> bytes_per_bit_pow_;
|
|
|
|
if (start_bit >= exclusive_end_bit) {
|
|
|
|
return;
|
|
|
|
}
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
assert(exclusive_end_bit > 0);
|
|
|
|
|
|
|
|
if (GetAndSet(start_bit) == 0) {
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
uint32_t new_useful_bytes = (exclusive_end_bit - start_bit)
|
|
|
|
<< bytes_per_bit_pow_;
|
|
|
|
RecordTick(GetStatistics(), READ_AMP_ESTIMATE_USEFUL_BYTES,
|
|
|
|
new_useful_bytes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Statistics* GetStatistics() {
|
|
|
|
return statistics_.load(std::memory_order_relaxed);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetStatistics(Statistics* stats) { statistics_.store(stats); }
|
|
|
|
|
|
|
|
uint32_t GetBytesPerBit() { return 1 << bytes_per_bit_pow_; }
|
|
|
|
|
|
|
|
size_t ApproximateMemoryUsage() const {
|
|
|
|
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
|
|
|
return malloc_usable_size((void*)this);
|
|
|
|
#endif // ROCKSDB_MALLOC_USABLE_SIZE
|
|
|
|
return sizeof(*this);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
// Get the current value of bit at `bit_idx` and set it to 1
|
|
|
|
inline bool GetAndSet(uint32_t bit_idx) {
|
|
|
|
const uint32_t byte_idx = bit_idx / kBitsPerEntry;
|
|
|
|
const uint32_t bit_mask = 1 << (bit_idx % kBitsPerEntry);
|
|
|
|
|
|
|
|
return bitmap_[byte_idx].fetch_or(bit_mask, std::memory_order_relaxed) &
|
|
|
|
bit_mask;
|
|
|
|
}
|
|
|
|
|
|
|
|
const uint32_t kBytesPersEntry = sizeof(uint32_t); // 4 bytes
|
|
|
|
const uint32_t kBitsPerEntry = kBytesPersEntry * 8; // 32 bits
|
|
|
|
|
|
|
|
// Bitmap used to record the bytes that we read, use atomic to protect
|
|
|
|
// against multiple threads updating the same bit
|
|
|
|
std::atomic<uint32_t>* bitmap_;
|
|
|
|
// (1 << bytes_per_bit_pow_) is bytes_per_bit. Use power of 2 to optimize
|
|
|
|
// muliplication and division
|
|
|
|
uint8_t bytes_per_bit_pow_;
|
|
|
|
// Pointer to DB Statistics object, Since this bitmap may outlive the DB
|
|
|
|
// this pointer maybe invalid, but the DB will update it to a valid pointer
|
|
|
|
// by using SetStatistics() before calling Mark()
|
|
|
|
std::atomic<Statistics*> statistics_;
|
unbiase readamp bitmap
Summary:
Consider BlockReadAmpBitmap with bytes_per_bit = 32. Suppose bytes [a, b) were used, while bytes [a-32, a)
and [b+1, b+33) weren't used; more formally, the union of ranges passed to BlockReadAmpBitmap::Mark() contains [a, b) and doesn't intersect with [a-32, a) and [b+1, b+33). Then bits [floor(a/32), ceil(b/32)] will be set, and so the number of useful bytes will be estimated as (ceil(b/32) - floor(a/32)) * 32, which is on average equal to b-a+31.
An extreme example: if we use 1 byte from each block, it'll be counted as 32 bytes from each block.
It's easy to remove this bias by slightly changing the semantics of the bitmap. Currently each bit represents a byte range [i*32, (i+1)*32).
This diff makes each bit represent a single byte: i*32 + X, where X is a random number in [0, 31] generated when bitmap is created. So, e.g., if you read a single byte at random, with probability 31/32 it won't be counted at all, and with probability 1/32 it will be counted as 32 bytes; so, on average it's counted as 1 byte.
*But there is one exception: the last bit will always set with the old way.*
(*) - assuming read_amp_bytes_per_bit = 32.
Closes https://github.com/facebook/rocksdb/pull/2259
Differential Revision: D5035652
Pulled By: lightmark
fbshipit-source-id: bd98b1b9b49fbe61f9e3781d07f624e3cbd92356
8 years ago
|
|
|
uint32_t rnd_;
|
|
|
|
};
|
|
|
|
|
|
|
|
// This Block class is not for any old block: it is designed to hold only
|
|
|
|
// uncompressed blocks containing sorted key-value pairs. It is thus
|
|
|
|
// suitable for storing uncompressed data blocks, index blocks (including
|
|
|
|
// partitions), range deletion blocks, properties blocks, metaindex blocks,
|
|
|
|
// as well as the top level of the partitioned filter structure (which is
|
|
|
|
// actually an index of the filter partitions). It is NOT suitable for
|
|
|
|
// compressed blocks in general, filter blocks/partitions, or compression
|
|
|
|
// dictionaries (since the latter do not contain sorted key-value pairs).
|
|
|
|
// Use BlockContents directly for those.
|
|
|
|
//
|
|
|
|
// See https://github.com/facebook/rocksdb/wiki/Rocksdb-BlockBasedTable-Format
|
|
|
|
// for details of the format and the various block types.
|
|
|
|
class Block {
|
|
|
|
public:
|
|
|
|
// Initialize the block with the specified contents.
|
|
|
|
explicit Block(BlockContents&& contents, size_t read_amp_bytes_per_bit = 0,
|
|
|
|
Statistics* statistics = nullptr);
|
|
|
|
// No copying allowed
|
|
|
|
Block(const Block&) = delete;
|
|
|
|
void operator=(const Block&) = delete;
|
|
|
|
|
|
|
|
~Block();
|
|
|
|
|
|
|
|
size_t size() const { return size_; }
|
|
|
|
const char* data() const { return data_; }
|
|
|
|
// The additional memory space taken by the block data.
|
|
|
|
size_t usable_size() const { return contents_.usable_size(); }
|
|
|
|
uint32_t NumRestarts() const;
|
|
|
|
bool own_bytes() const { return contents_.own_bytes(); }
|
|
|
|
|
|
|
|
BlockBasedTableOptions::DataBlockIndexType IndexType() const;
|
|
|
|
|
|
|
|
// raw_ucmp is a raw (i.e., not wrapped by `UserComparatorWrapper`) user key
|
|
|
|
// comparator.
|
|
|
|
//
|
|
|
|
// If iter is null, return new Iterator
|
|
|
|
// If iter is not null, update this one and return it as Iterator*
|
|
|
|
//
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
// Updates read_amp_bitmap_ if it is not nullptr.
|
|
|
|
//
|
|
|
|
// If `block_contents_pinned` is true, the caller will guarantee that when
|
|
|
|
// the cleanup functions are transferred from the iterator to other
|
|
|
|
// classes, e.g. PinnableSlice, the pointer to the bytes will still be
|
|
|
|
// valid. Either the iterator holds cache handle or ownership of some resource
|
|
|
|
// and release them in a release function, or caller is sure that the data
|
|
|
|
// will not go away (for example, it's from mmapped file which will not be
|
|
|
|
// closed).
|
|
|
|
//
|
|
|
|
// NOTE: for the hash based lookup, if a key prefix doesn't match any key,
|
|
|
|
// the iterator will simply be set as "invalid", rather than returning
|
|
|
|
// the key that is just pass the target key.
|
|
|
|
DataBlockIter* NewDataIterator(const Comparator* raw_ucmp,
|
|
|
|
SequenceNumber global_seqno,
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
DataBlockIter* iter = nullptr,
|
|
|
|
Statistics* stats = nullptr,
|
|
|
|
bool block_contents_pinned = false);
|
|
|
|
|
|
|
|
// Returns an MetaBlockIter for iterating over blocks containing metadata
|
|
|
|
// (like Properties blocks). Unlike data blocks, the keys for these blocks
|
|
|
|
// do not contain sequence numbers, do not use a user-define comparator, and
|
|
|
|
// do not track read amplification/statistics. Additionally, MetaBlocks will
|
|
|
|
// not assert if the block is formatted improperly.
|
|
|
|
//
|
|
|
|
// If `block_contents_pinned` is true, the caller will guarantee that when
|
|
|
|
// the cleanup functions are transferred from the iterator to other
|
|
|
|
// classes, e.g. PinnableSlice, the pointer to the bytes will still be
|
|
|
|
// valid. Either the iterator holds cache handle or ownership of some resource
|
|
|
|
// and release them in a release function, or caller is sure that the data
|
|
|
|
// will not go away (for example, it's from mmapped file which will not be
|
|
|
|
// closed).
|
|
|
|
MetaBlockIter* NewMetaIterator(bool block_contents_pinned = false);
|
|
|
|
|
|
|
|
// raw_ucmp is a raw (i.e., not wrapped by `UserComparatorWrapper`) user key
|
|
|
|
// comparator.
|
|
|
|
//
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
// key_includes_seq, default true, means that the keys are in internal key
|
|
|
|
// format.
|
|
|
|
// value_is_full, default true, means that no delta encoding is
|
|
|
|
// applied to values.
|
|
|
|
//
|
|
|
|
// If `prefix_index` is not nullptr this block will do hash lookup for the key
|
|
|
|
// prefix. If total_order_seek is true, prefix_index_ is ignored.
|
|
|
|
//
|
|
|
|
// `have_first_key` controls whether IndexValue will contain
|
|
|
|
// first_internal_key. It affects data serialization format, so the same value
|
|
|
|
// have_first_key must be used when writing and reading index.
|
|
|
|
// It is determined by IndexType property of the table.
|
|
|
|
IndexBlockIter* NewIndexIterator(const Comparator* raw_ucmp,
|
|
|
|
SequenceNumber global_seqno,
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
IndexBlockIter* iter, Statistics* stats,
|
|
|
|
bool total_order_seek, bool have_first_key,
|
|
|
|
bool key_includes_seq, bool value_is_full,
|
|
|
|
bool block_contents_pinned = false,
|
|
|
|
BlockPrefixIndex* prefix_index = nullptr);
|
|
|
|
|
|
|
|
// Report an approximation of how much memory has been used.
|
|
|
|
size_t ApproximateMemoryUsage() const;
|
|
|
|
|
|
|
|
private:
|
|
|
|
BlockContents contents_;
|
|
|
|
const char* data_; // contents_.data.data()
|
|
|
|
size_t size_; // contents_.data.size()
|
|
|
|
uint32_t restart_offset_; // Offset in data_ of restart array
|
|
|
|
uint32_t num_restarts_;
|
|
|
|
std::unique_ptr<BlockReadAmpBitmap> read_amp_bitmap_;
|
|
|
|
DataBlockHashIndex data_block_hash_index_;
|
|
|
|
};
|
|
|
|
|
|
|
|
// A `BlockIter` iterates over the entries in a `Block`'s data buffer. The
|
|
|
|
// format of this data buffer is an uncompressed, sorted sequence of key-value
|
|
|
|
// pairs (see `Block` API for more details).
|
|
|
|
//
|
|
|
|
// Notably, the keys may either be in internal key format or user key format.
|
|
|
|
// Subclasses are responsible for configuring the key format.
|
|
|
|
//
|
|
|
|
// `BlockIter` intends to provide final overrides for all of
|
|
|
|
// `InternalIteratorBase` functions that can move the iterator. It does
|
|
|
|
// this to guarantee `UpdateKey()` is called exactly once after each key
|
|
|
|
// movement potentially visible to users. In this step, the key is prepared
|
|
|
|
// (e.g., serialized if global seqno is in effect) so it can be returned
|
|
|
|
// immediately when the user asks for it via calling `key() const`.
|
|
|
|
//
|
|
|
|
// For its subclasses, it provides protected variants of the above-mentioned
|
|
|
|
// final-overridden methods. They are named with the "Impl" suffix, e.g.,
|
|
|
|
// `Seek()` logic would be implemented by subclasses in `SeekImpl()`. These
|
|
|
|
// "Impl" functions are responsible for positioning `raw_key_` but not
|
|
|
|
// invoking `UpdateKey()`.
|
|
|
|
template <class TValue>
|
|
|
|
class BlockIter : public InternalIteratorBase<TValue> {
|
|
|
|
public:
|
|
|
|
void InitializeBase(const Comparator* raw_ucmp, const char* data,
|
|
|
|
uint32_t restarts, uint32_t num_restarts,
|
|
|
|
SequenceNumber global_seqno, bool block_contents_pinned) {
|
|
|
|
assert(data_ == nullptr); // Ensure it is called only once
|
|
|
|
assert(num_restarts > 0); // Ensure the param is valid
|
|
|
|
|
|
|
|
raw_ucmp_ = raw_ucmp;
|
|
|
|
data_ = data;
|
|
|
|
restarts_ = restarts;
|
|
|
|
num_restarts_ = num_restarts;
|
|
|
|
current_ = restarts_;
|
|
|
|
restart_index_ = num_restarts_;
|
|
|
|
global_seqno_ = global_seqno;
|
Copy Get() result when file reads use mmap
Summary:
For iterator reads, a `SuperVersion` is pinned to preserve a snapshot of SST files, and `Block`s are pinned to allow `key()` and `value()` to return pointers directly into a RocksDB memory region. This works for both non-mmap reads, where the block owns the memory region, and mmap reads, where the file owns the memory region.
For point reads with `PinnableSlice`, only the `Block` object is pinned. This works for non-mmap reads because the block owns the memory region, so even if the file is deleted after compaction, the memory region survives. However, for mmap reads, file deletion causes the memory region to which the `PinnableSlice` refers to be unmapped. The result is usually a segfault upon accessing the `PinnableSlice`, although sometimes it returned wrong results (I repro'd this a bunch of times with `db_stress`).
This PR copies the value into the `PinnableSlice` when it comes from mmap'd memory. We can tell whether the `Block` owns its memory using `Block::cachable()`, which is unset when reads do not use the provided buffer as is the case with mmap file reads. When that is false we ensure the result of `Get()` is copied.
This feels like a short-term solution as ideally we'd have the `PinnableSlice` pin the mmap'd memory so we can do zero-copy reads. It seemed hard so I chose this approach to fix correctness in the meantime.
Closes https://github.com/facebook/rocksdb/pull/3881
Differential Revision: D8076288
Pulled By: ajkr
fbshipit-source-id: 31d78ec010198723522323dbc6ea325122a46b08
7 years ago
|
|
|
block_contents_pinned_ = block_contents_pinned;
|
|
|
|
cache_handle_ = nullptr;
|
|
|
|
}
|
|
|
|
|
Change and clarify the relationship between Valid(), status() and Seek*() for all iterators. Also fix some bugs
Summary:
Before this PR, Iterator/InternalIterator may simultaneously have non-ok status() and Valid() = true. That state means that the last operation failed, but the iterator is nevertheless positioned on some unspecified record. Likely intended uses of that are:
* If some sst files are corrupted, a normal iterator can be used to read the data from files that are not corrupted.
* When using read_tier = kBlockCacheTier, read the data that's in block cache, skipping over the data that is not.
However, this behavior wasn't documented well (and until recently the wiki on github had misleading incorrect information). In the code there's a lot of confusion about the relationship between status() and Valid(), and about whether Seek()/SeekToLast()/etc reset the status or not. There were a number of bugs caused by this confusion, both inside rocksdb and in the code that uses rocksdb (including ours).
This PR changes the convention to:
* If status() is not ok, Valid() always returns false.
* Any seek operation resets status. (Before the PR, it depended on iterator type and on particular error.)
This does sacrifice the two use cases listed above, but siying said it's ok.
Overview of the changes:
* A commit that adds missing status checks in MergingIterator. This fixes a bug that actually affects us, and we need it fixed. `DBIteratorTest.NonBlockingIterationBugRepro` explains the scenario.
* Changes to lots of iterator types to make all of them conform to the new convention. Some bug fixes along the way. By far the biggest changes are in DBIter, which is a big messy piece of code; I tried to make it less big and messy but mostly failed.
* A stress-test for DBIter, to gain some confidence that I didn't break it. It does a few million random operations on the iterator, while occasionally modifying the underlying data (like ForwardIterator does) and occasionally returning non-ok status from internal iterator.
To find the iterator types that needed changes I searched for "public .*Iterator" in the code. Here's an overview of all 27 iterator types:
Iterators that didn't need changes:
* status() is always ok(), or Valid() is always false: MemTableIterator, ModelIter, TestIterator, KVIter (2 classes with this name anonymous namespaces), LoggingForwardVectorIterator, VectorIterator, MockTableIterator, EmptyIterator, EmptyInternalIterator.
* Thin wrappers that always pass through Valid() and status(): ArenaWrappedDBIter, TtlIterator, InternalIteratorFromIterator.
Iterators with changes (see inline comments for details):
* DBIter - an overhaul:
- It used to silently skip corrupted keys (`FindParseableKey()`), which seems dangerous. This PR makes it just stop immediately after encountering a corrupted key, just like it would for other kinds of corruption. Let me know if there was actually some deeper meaning in this behavior and I should put it back.
- It had a few code paths silently discarding subiterator's status. The stress test caught a few.
- The backwards iteration code path was expecting the internal iterator's set of keys to be immutable. It's probably always true in practice at the moment, since ForwardIterator doesn't support backwards iteration, but this PR fixes it anyway. See added DBIteratorTest.ReverseToForwardBug for an example.
- Some parts of backwards iteration code path even did things like `assert(iter_->Valid())` after a seek, which is never a safe assumption.
- It used to not reset status on seek for some types of errors.
- Some simplifications and better comments.
- Some things got more complicated from the added error handling. I'm open to ideas for how to make it nicer.
* MergingIterator - check status after every operation on every subiterator, and in some places assert that valid subiterators have ok status.
* ForwardIterator - changed to the new convention, also slightly simplified.
* ForwardLevelIterator - fixed some bugs and simplified.
* LevelIterator - simplified.
* TwoLevelIterator - changed to the new convention. Also fixed a bug that would make SeekForPrev() sometimes silently ignore errors from first_level_iter_.
* BlockBasedTableIterator - minor changes.
* BlockIter - replaced `SetStatus()` with `Invalidate()` to make sure non-ok BlockIter is always invalid.
* PlainTableIterator - some seeks used to not reset status.
* CuckooTableIterator - tiny code cleanup.
* ManagedIterator - fixed some bugs.
* BaseDeltaIterator - changed to the new convention and fixed a bug.
* BlobDBIterator - seeks used to not reset status.
* KeyConvertingIterator - some small change.
Closes https://github.com/facebook/rocksdb/pull/3810
Differential Revision: D7888019
Pulled By: al13n321
fbshipit-source-id: 4aaf6d3421c545d16722a815b2fa2e7912bc851d
7 years ago
|
|
|
// Makes Valid() return false, status() return `s`, and Seek()/Prev()/etc do
|
|
|
|
// nothing. Calls cleanup functions.
|
|
|
|
virtual void Invalidate(const Status& s) {
|
Change and clarify the relationship between Valid(), status() and Seek*() for all iterators. Also fix some bugs
Summary:
Before this PR, Iterator/InternalIterator may simultaneously have non-ok status() and Valid() = true. That state means that the last operation failed, but the iterator is nevertheless positioned on some unspecified record. Likely intended uses of that are:
* If some sst files are corrupted, a normal iterator can be used to read the data from files that are not corrupted.
* When using read_tier = kBlockCacheTier, read the data that's in block cache, skipping over the data that is not.
However, this behavior wasn't documented well (and until recently the wiki on github had misleading incorrect information). In the code there's a lot of confusion about the relationship between status() and Valid(), and about whether Seek()/SeekToLast()/etc reset the status or not. There were a number of bugs caused by this confusion, both inside rocksdb and in the code that uses rocksdb (including ours).
This PR changes the convention to:
* If status() is not ok, Valid() always returns false.
* Any seek operation resets status. (Before the PR, it depended on iterator type and on particular error.)
This does sacrifice the two use cases listed above, but siying said it's ok.
Overview of the changes:
* A commit that adds missing status checks in MergingIterator. This fixes a bug that actually affects us, and we need it fixed. `DBIteratorTest.NonBlockingIterationBugRepro` explains the scenario.
* Changes to lots of iterator types to make all of them conform to the new convention. Some bug fixes along the way. By far the biggest changes are in DBIter, which is a big messy piece of code; I tried to make it less big and messy but mostly failed.
* A stress-test for DBIter, to gain some confidence that I didn't break it. It does a few million random operations on the iterator, while occasionally modifying the underlying data (like ForwardIterator does) and occasionally returning non-ok status from internal iterator.
To find the iterator types that needed changes I searched for "public .*Iterator" in the code. Here's an overview of all 27 iterator types:
Iterators that didn't need changes:
* status() is always ok(), or Valid() is always false: MemTableIterator, ModelIter, TestIterator, KVIter (2 classes with this name anonymous namespaces), LoggingForwardVectorIterator, VectorIterator, MockTableIterator, EmptyIterator, EmptyInternalIterator.
* Thin wrappers that always pass through Valid() and status(): ArenaWrappedDBIter, TtlIterator, InternalIteratorFromIterator.
Iterators with changes (see inline comments for details):
* DBIter - an overhaul:
- It used to silently skip corrupted keys (`FindParseableKey()`), which seems dangerous. This PR makes it just stop immediately after encountering a corrupted key, just like it would for other kinds of corruption. Let me know if there was actually some deeper meaning in this behavior and I should put it back.
- It had a few code paths silently discarding subiterator's status. The stress test caught a few.
- The backwards iteration code path was expecting the internal iterator's set of keys to be immutable. It's probably always true in practice at the moment, since ForwardIterator doesn't support backwards iteration, but this PR fixes it anyway. See added DBIteratorTest.ReverseToForwardBug for an example.
- Some parts of backwards iteration code path even did things like `assert(iter_->Valid())` after a seek, which is never a safe assumption.
- It used to not reset status on seek for some types of errors.
- Some simplifications and better comments.
- Some things got more complicated from the added error handling. I'm open to ideas for how to make it nicer.
* MergingIterator - check status after every operation on every subiterator, and in some places assert that valid subiterators have ok status.
* ForwardIterator - changed to the new convention, also slightly simplified.
* ForwardLevelIterator - fixed some bugs and simplified.
* LevelIterator - simplified.
* TwoLevelIterator - changed to the new convention. Also fixed a bug that would make SeekForPrev() sometimes silently ignore errors from first_level_iter_.
* BlockBasedTableIterator - minor changes.
* BlockIter - replaced `SetStatus()` with `Invalidate()` to make sure non-ok BlockIter is always invalid.
* PlainTableIterator - some seeks used to not reset status.
* CuckooTableIterator - tiny code cleanup.
* ManagedIterator - fixed some bugs.
* BaseDeltaIterator - changed to the new convention and fixed a bug.
* BlobDBIterator - seeks used to not reset status.
* KeyConvertingIterator - some small change.
Closes https://github.com/facebook/rocksdb/pull/3810
Differential Revision: D7888019
Pulled By: al13n321
fbshipit-source-id: 4aaf6d3421c545d16722a815b2fa2e7912bc851d
7 years ago
|
|
|
// Assert that the BlockIter is never deleted while Pinning is Enabled.
|
|
|
|
assert(!pinned_iters_mgr_ || !pinned_iters_mgr_->PinningEnabled());
|
Change and clarify the relationship between Valid(), status() and Seek*() for all iterators. Also fix some bugs
Summary:
Before this PR, Iterator/InternalIterator may simultaneously have non-ok status() and Valid() = true. That state means that the last operation failed, but the iterator is nevertheless positioned on some unspecified record. Likely intended uses of that are:
* If some sst files are corrupted, a normal iterator can be used to read the data from files that are not corrupted.
* When using read_tier = kBlockCacheTier, read the data that's in block cache, skipping over the data that is not.
However, this behavior wasn't documented well (and until recently the wiki on github had misleading incorrect information). In the code there's a lot of confusion about the relationship between status() and Valid(), and about whether Seek()/SeekToLast()/etc reset the status or not. There were a number of bugs caused by this confusion, both inside rocksdb and in the code that uses rocksdb (including ours).
This PR changes the convention to:
* If status() is not ok, Valid() always returns false.
* Any seek operation resets status. (Before the PR, it depended on iterator type and on particular error.)
This does sacrifice the two use cases listed above, but siying said it's ok.
Overview of the changes:
* A commit that adds missing status checks in MergingIterator. This fixes a bug that actually affects us, and we need it fixed. `DBIteratorTest.NonBlockingIterationBugRepro` explains the scenario.
* Changes to lots of iterator types to make all of them conform to the new convention. Some bug fixes along the way. By far the biggest changes are in DBIter, which is a big messy piece of code; I tried to make it less big and messy but mostly failed.
* A stress-test for DBIter, to gain some confidence that I didn't break it. It does a few million random operations on the iterator, while occasionally modifying the underlying data (like ForwardIterator does) and occasionally returning non-ok status from internal iterator.
To find the iterator types that needed changes I searched for "public .*Iterator" in the code. Here's an overview of all 27 iterator types:
Iterators that didn't need changes:
* status() is always ok(), or Valid() is always false: MemTableIterator, ModelIter, TestIterator, KVIter (2 classes with this name anonymous namespaces), LoggingForwardVectorIterator, VectorIterator, MockTableIterator, EmptyIterator, EmptyInternalIterator.
* Thin wrappers that always pass through Valid() and status(): ArenaWrappedDBIter, TtlIterator, InternalIteratorFromIterator.
Iterators with changes (see inline comments for details):
* DBIter - an overhaul:
- It used to silently skip corrupted keys (`FindParseableKey()`), which seems dangerous. This PR makes it just stop immediately after encountering a corrupted key, just like it would for other kinds of corruption. Let me know if there was actually some deeper meaning in this behavior and I should put it back.
- It had a few code paths silently discarding subiterator's status. The stress test caught a few.
- The backwards iteration code path was expecting the internal iterator's set of keys to be immutable. It's probably always true in practice at the moment, since ForwardIterator doesn't support backwards iteration, but this PR fixes it anyway. See added DBIteratorTest.ReverseToForwardBug for an example.
- Some parts of backwards iteration code path even did things like `assert(iter_->Valid())` after a seek, which is never a safe assumption.
- It used to not reset status on seek for some types of errors.
- Some simplifications and better comments.
- Some things got more complicated from the added error handling. I'm open to ideas for how to make it nicer.
* MergingIterator - check status after every operation on every subiterator, and in some places assert that valid subiterators have ok status.
* ForwardIterator - changed to the new convention, also slightly simplified.
* ForwardLevelIterator - fixed some bugs and simplified.
* LevelIterator - simplified.
* TwoLevelIterator - changed to the new convention. Also fixed a bug that would make SeekForPrev() sometimes silently ignore errors from first_level_iter_.
* BlockBasedTableIterator - minor changes.
* BlockIter - replaced `SetStatus()` with `Invalidate()` to make sure non-ok BlockIter is always invalid.
* PlainTableIterator - some seeks used to not reset status.
* CuckooTableIterator - tiny code cleanup.
* ManagedIterator - fixed some bugs.
* BaseDeltaIterator - changed to the new convention and fixed a bug.
* BlobDBIterator - seeks used to not reset status.
* KeyConvertingIterator - some small change.
Closes https://github.com/facebook/rocksdb/pull/3810
Differential Revision: D7888019
Pulled By: al13n321
fbshipit-source-id: 4aaf6d3421c545d16722a815b2fa2e7912bc851d
7 years ago
|
|
|
|
|
|
|
data_ = nullptr;
|
|
|
|
current_ = restarts_;
|
|
|
|
status_ = s;
|
Change and clarify the relationship between Valid(), status() and Seek*() for all iterators. Also fix some bugs
Summary:
Before this PR, Iterator/InternalIterator may simultaneously have non-ok status() and Valid() = true. That state means that the last operation failed, but the iterator is nevertheless positioned on some unspecified record. Likely intended uses of that are:
* If some sst files are corrupted, a normal iterator can be used to read the data from files that are not corrupted.
* When using read_tier = kBlockCacheTier, read the data that's in block cache, skipping over the data that is not.
However, this behavior wasn't documented well (and until recently the wiki on github had misleading incorrect information). In the code there's a lot of confusion about the relationship between status() and Valid(), and about whether Seek()/SeekToLast()/etc reset the status or not. There were a number of bugs caused by this confusion, both inside rocksdb and in the code that uses rocksdb (including ours).
This PR changes the convention to:
* If status() is not ok, Valid() always returns false.
* Any seek operation resets status. (Before the PR, it depended on iterator type and on particular error.)
This does sacrifice the two use cases listed above, but siying said it's ok.
Overview of the changes:
* A commit that adds missing status checks in MergingIterator. This fixes a bug that actually affects us, and we need it fixed. `DBIteratorTest.NonBlockingIterationBugRepro` explains the scenario.
* Changes to lots of iterator types to make all of them conform to the new convention. Some bug fixes along the way. By far the biggest changes are in DBIter, which is a big messy piece of code; I tried to make it less big and messy but mostly failed.
* A stress-test for DBIter, to gain some confidence that I didn't break it. It does a few million random operations on the iterator, while occasionally modifying the underlying data (like ForwardIterator does) and occasionally returning non-ok status from internal iterator.
To find the iterator types that needed changes I searched for "public .*Iterator" in the code. Here's an overview of all 27 iterator types:
Iterators that didn't need changes:
* status() is always ok(), or Valid() is always false: MemTableIterator, ModelIter, TestIterator, KVIter (2 classes with this name anonymous namespaces), LoggingForwardVectorIterator, VectorIterator, MockTableIterator, EmptyIterator, EmptyInternalIterator.
* Thin wrappers that always pass through Valid() and status(): ArenaWrappedDBIter, TtlIterator, InternalIteratorFromIterator.
Iterators with changes (see inline comments for details):
* DBIter - an overhaul:
- It used to silently skip corrupted keys (`FindParseableKey()`), which seems dangerous. This PR makes it just stop immediately after encountering a corrupted key, just like it would for other kinds of corruption. Let me know if there was actually some deeper meaning in this behavior and I should put it back.
- It had a few code paths silently discarding subiterator's status. The stress test caught a few.
- The backwards iteration code path was expecting the internal iterator's set of keys to be immutable. It's probably always true in practice at the moment, since ForwardIterator doesn't support backwards iteration, but this PR fixes it anyway. See added DBIteratorTest.ReverseToForwardBug for an example.
- Some parts of backwards iteration code path even did things like `assert(iter_->Valid())` after a seek, which is never a safe assumption.
- It used to not reset status on seek for some types of errors.
- Some simplifications and better comments.
- Some things got more complicated from the added error handling. I'm open to ideas for how to make it nicer.
* MergingIterator - check status after every operation on every subiterator, and in some places assert that valid subiterators have ok status.
* ForwardIterator - changed to the new convention, also slightly simplified.
* ForwardLevelIterator - fixed some bugs and simplified.
* LevelIterator - simplified.
* TwoLevelIterator - changed to the new convention. Also fixed a bug that would make SeekForPrev() sometimes silently ignore errors from first_level_iter_.
* BlockBasedTableIterator - minor changes.
* BlockIter - replaced `SetStatus()` with `Invalidate()` to make sure non-ok BlockIter is always invalid.
* PlainTableIterator - some seeks used to not reset status.
* CuckooTableIterator - tiny code cleanup.
* ManagedIterator - fixed some bugs.
* BaseDeltaIterator - changed to the new convention and fixed a bug.
* BlobDBIterator - seeks used to not reset status.
* KeyConvertingIterator - some small change.
Closes https://github.com/facebook/rocksdb/pull/3810
Differential Revision: D7888019
Pulled By: al13n321
fbshipit-source-id: 4aaf6d3421c545d16722a815b2fa2e7912bc851d
7 years ago
|
|
|
|
|
|
|
// Call cleanup callbacks.
|
|
|
|
Cleanable::Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Valid() const override { return current_ < restarts_; }
|
|
|
|
|
|
|
|
virtual void SeekToFirst() override final {
|
|
|
|
SeekToFirstImpl();
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void SeekToLast() override final {
|
|
|
|
SeekToLastImpl();
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void Seek(const Slice& target) override final {
|
|
|
|
SeekImpl(target);
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void SeekForPrev(const Slice& target) override final {
|
|
|
|
SeekForPrevImpl(target);
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void Next() override final {
|
|
|
|
NextImpl();
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual bool NextAndGetResult(IterateResult* result) override final {
|
|
|
|
// This does not need to call `UpdateKey()` as the parent class only has
|
|
|
|
// access to the `UpdateKey()`-invoking functions.
|
|
|
|
return InternalIteratorBase<TValue>::NextAndGetResult(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void Prev() override final {
|
|
|
|
PrevImpl();
|
|
|
|
UpdateKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
Status status() const override { return status_; }
|
|
|
|
Slice key() const override {
|
|
|
|
assert(Valid());
|
|
|
|
return key_;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifndef NDEBUG
|
|
|
|
~BlockIter() override {
|
|
|
|
// Assert that the BlockIter is never deleted while Pinning is Enabled.
|
|
|
|
assert(!pinned_iters_mgr_ ||
|
|
|
|
(pinned_iters_mgr_ && !pinned_iters_mgr_->PinningEnabled()));
|
|
|
|
status_.PermitUncheckedError();
|
|
|
|
}
|
|
|
|
void SetPinnedItersMgr(PinnedIteratorsManager* pinned_iters_mgr) override {
|
|
|
|
pinned_iters_mgr_ = pinned_iters_mgr;
|
|
|
|
}
|
|
|
|
PinnedIteratorsManager* pinned_iters_mgr_ = nullptr;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
bool IsKeyPinned() const override {
|
Copy Get() result when file reads use mmap
Summary:
For iterator reads, a `SuperVersion` is pinned to preserve a snapshot of SST files, and `Block`s are pinned to allow `key()` and `value()` to return pointers directly into a RocksDB memory region. This works for both non-mmap reads, where the block owns the memory region, and mmap reads, where the file owns the memory region.
For point reads with `PinnableSlice`, only the `Block` object is pinned. This works for non-mmap reads because the block owns the memory region, so even if the file is deleted after compaction, the memory region survives. However, for mmap reads, file deletion causes the memory region to which the `PinnableSlice` refers to be unmapped. The result is usually a segfault upon accessing the `PinnableSlice`, although sometimes it returned wrong results (I repro'd this a bunch of times with `db_stress`).
This PR copies the value into the `PinnableSlice` when it comes from mmap'd memory. We can tell whether the `Block` owns its memory using `Block::cachable()`, which is unset when reads do not use the provided buffer as is the case with mmap file reads. When that is false we ensure the result of `Get()` is copied.
This feels like a short-term solution as ideally we'd have the `PinnableSlice` pin the mmap'd memory so we can do zero-copy reads. It seemed hard so I chose this approach to fix correctness in the meantime.
Closes https://github.com/facebook/rocksdb/pull/3881
Differential Revision: D8076288
Pulled By: ajkr
fbshipit-source-id: 31d78ec010198723522323dbc6ea325122a46b08
7 years ago
|
|
|
return block_contents_pinned_ && key_pinned_;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsValuePinned() const override { return block_contents_pinned_; }
|
Introduce FullMergeV2 (eliminate memcpy from merge operators)
Summary:
This diff update the code to pin the merge operator operands while the merge operation is done, so that we can eliminate the memcpy cost, to do that we need a new public API for FullMerge that replace the std::deque<std::string> with std::vector<Slice>
This diff is stacked on top of D56493 and D56511
In this diff we
- Update FullMergeV2 arguments to be encapsulated in MergeOperationInput and MergeOperationOutput which will make it easier to add new arguments in the future
- Replace std::deque<std::string> with std::vector<Slice> to pass operands
- Replace MergeContext std::deque with std::vector (based on a simple benchmark I ran https://gist.github.com/IslamAbdelRahman/78fc86c9ab9f52b1df791e58943fb187)
- Allow FullMergeV2 output to be an existing operand
```
[Everything in Memtable | 10K operands | 10 KB each | 1 operand per key]
DEBUG_LEVEL=0 make db_bench -j64 && ./db_bench --benchmarks="mergerandom,readseq,readseq,readseq,readseq,readseq" --merge_operator="max" --merge_keys=10000 --num=10000 --disable_auto_compactions --value_size=10240 --write_buffer_size=1000000000
[FullMergeV2]
readseq : 0.607 micros/op 1648235 ops/sec; 16121.2 MB/s
readseq : 0.478 micros/op 2091546 ops/sec; 20457.2 MB/s
readseq : 0.252 micros/op 3972081 ops/sec; 38850.5 MB/s
readseq : 0.237 micros/op 4218328 ops/sec; 41259.0 MB/s
readseq : 0.247 micros/op 4043927 ops/sec; 39553.2 MB/s
[master]
readseq : 3.935 micros/op 254140 ops/sec; 2485.7 MB/s
readseq : 3.722 micros/op 268657 ops/sec; 2627.7 MB/s
readseq : 3.149 micros/op 317605 ops/sec; 3106.5 MB/s
readseq : 3.125 micros/op 320024 ops/sec; 3130.1 MB/s
readseq : 4.075 micros/op 245374 ops/sec; 2400.0 MB/s
```
```
[Everything in Memtable | 10K operands | 10 KB each | 10 operand per key]
DEBUG_LEVEL=0 make db_bench -j64 && ./db_bench --benchmarks="mergerandom,readseq,readseq,readseq,readseq,readseq" --merge_operator="max" --merge_keys=1000 --num=10000 --disable_auto_compactions --value_size=10240 --write_buffer_size=1000000000
[FullMergeV2]
readseq : 3.472 micros/op 288018 ops/sec; 2817.1 MB/s
readseq : 2.304 micros/op 434027 ops/sec; 4245.2 MB/s
readseq : 1.163 micros/op 859845 ops/sec; 8410.0 MB/s
readseq : 1.192 micros/op 838926 ops/sec; 8205.4 MB/s
readseq : 1.250 micros/op 800000 ops/sec; 7824.7 MB/s
[master]
readseq : 24.025 micros/op 41623 ops/sec; 407.1 MB/s
readseq : 18.489 micros/op 54086 ops/sec; 529.0 MB/s
readseq : 18.693 micros/op 53495 ops/sec; 523.2 MB/s
readseq : 23.621 micros/op 42335 ops/sec; 414.1 MB/s
readseq : 18.775 micros/op 53262 ops/sec; 521.0 MB/s
```
```
[Everything in Block cache | 10K operands | 10 KB each | 1 operand per key]
[FullMergeV2]
$ DEBUG_LEVEL=0 make db_bench -j64 && ./db_bench --benchmarks="readseq,readseq,readseq,readseq,readseq" --merge_operator="max" --num=100000 --db="/dev/shm/merge-random-10K-10KB" --cache_size=1000000000 --use_existing_db --disable_auto_compactions
readseq : 14.741 micros/op 67837 ops/sec; 663.5 MB/s
readseq : 1.029 micros/op 971446 ops/sec; 9501.6 MB/s
readseq : 0.974 micros/op 1026229 ops/sec; 10037.4 MB/s
readseq : 0.965 micros/op 1036080 ops/sec; 10133.8 MB/s
readseq : 0.943 micros/op 1060657 ops/sec; 10374.2 MB/s
[master]
readseq : 16.735 micros/op 59755 ops/sec; 584.5 MB/s
readseq : 3.029 micros/op 330151 ops/sec; 3229.2 MB/s
readseq : 3.136 micros/op 318883 ops/sec; 3119.0 MB/s
readseq : 3.065 micros/op 326245 ops/sec; 3191.0 MB/s
readseq : 3.014 micros/op 331813 ops/sec; 3245.4 MB/s
```
```
[Everything in Block cache | 10K operands | 10 KB each | 10 operand per key]
DEBUG_LEVEL=0 make db_bench -j64 && ./db_bench --benchmarks="readseq,readseq,readseq,readseq,readseq" --merge_operator="max" --num=100000 --db="/dev/shm/merge-random-10-operands-10K-10KB" --cache_size=1000000000 --use_existing_db --disable_auto_compactions
[FullMergeV2]
readseq : 24.325 micros/op 41109 ops/sec; 402.1 MB/s
readseq : 1.470 micros/op 680272 ops/sec; 6653.7 MB/s
readseq : 1.231 micros/op 812347 ops/sec; 7945.5 MB/s
readseq : 1.091 micros/op 916590 ops/sec; 8965.1 MB/s
readseq : 1.109 micros/op 901713 ops/sec; 8819.6 MB/s
[master]
readseq : 27.257 micros/op 36687 ops/sec; 358.8 MB/s
readseq : 4.443 micros/op 225073 ops/sec; 2201.4 MB/s
readseq : 5.830 micros/op 171526 ops/sec; 1677.7 MB/s
readseq : 4.173 micros/op 239635 ops/sec; 2343.8 MB/s
readseq : 4.150 micros/op 240963 ops/sec; 2356.8 MB/s
```
Test Plan: COMPILE_WITH_ASAN=1 make check -j64
Reviewers: yhchiang, andrewkr, sdong
Reviewed By: sdong
Subscribers: lovro, andrewkr, dhruba
Differential Revision: https://reviews.facebook.net/D57075
8 years ago
|
|
|
|
|
|
|
size_t TEST_CurrentEntrySize() { return NextEntryOffset() - current_; }
|
|
|
|
|
|
|
|
uint32_t ValueOffset() const {
|
|
|
|
return static_cast<uint32_t>(value_.data() - data_);
|
|
|
|
}
|
|
|
|
|
|
|
|
void SetCacheHandle(Cache::Handle* handle) { cache_handle_ = handle; }
|
|
|
|
|
|
|
|
Cache::Handle* cache_handle() { return cache_handle_; }
|
|
|
|
|
|
|
|
protected:
|
|
|
|
const char* data_; // underlying block contents
|
|
|
|
uint32_t num_restarts_; // Number of uint32_t entries in restart array
|
|
|
|
|
|
|
|
// Index of restart block in which current_ or current_-1 falls
|
|
|
|
uint32_t restart_index_;
|
|
|
|
uint32_t restarts_; // Offset of restart array (list of fixed32)
|
|
|
|
// current_ is offset in data_ of current entry. >= restarts_ if !Valid
|
|
|
|
uint32_t current_;
|
|
|
|
// Raw key from block.
|
|
|
|
IterKey raw_key_;
|
|
|
|
// Buffer for key data when global seqno assignment is enabled.
|
|
|
|
IterKey key_buf_;
|
|
|
|
Slice value_;
|
|
|
|
Status status_;
|
|
|
|
// Key to be exposed to users.
|
|
|
|
Slice key_;
|
|
|
|
bool key_pinned_;
|
|
|
|
// Whether the block data is guaranteed to outlive this iterator, and
|
|
|
|
// as long as the cleanup functions are transferred to another class,
|
|
|
|
// e.g. PinnableSlice, the pointer to the bytes will still be valid.
|
|
|
|
bool block_contents_pinned_;
|
|
|
|
SequenceNumber global_seqno_;
|
|
|
|
|
|
|
|
virtual void SeekToFirstImpl() = 0;
|
|
|
|
virtual void SeekToLastImpl() = 0;
|
|
|
|
virtual void SeekImpl(const Slice& target) = 0;
|
|
|
|
virtual void SeekForPrevImpl(const Slice& target) = 0;
|
|
|
|
virtual void NextImpl() = 0;
|
|
|
|
|
|
|
|
virtual void PrevImpl() = 0;
|
|
|
|
|
|
|
|
template <typename DecodeEntryFunc>
|
|
|
|
inline bool ParseNextKey(bool* is_shared);
|
|
|
|
|
|
|
|
InternalKeyComparator icmp() {
|
|
|
|
return InternalKeyComparator(raw_ucmp_, false /* named */);
|
|
|
|
}
|
|
|
|
|
|
|
|
UserComparatorWrapper ucmp() { return UserComparatorWrapper(raw_ucmp_); }
|
|
|
|
|
|
|
|
// Must be called every time a key is found that needs to be returned to user,
|
|
|
|
// and may be called when no key is found (as a no-op). Updates `key_`,
|
|
|
|
// `key_buf_`, and `key_pinned_` with info about the found key.
|
|
|
|
void UpdateKey() {
|
|
|
|
key_buf_.Clear();
|
|
|
|
if (!Valid()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (raw_key_.IsUserKey()) {
|
|
|
|
assert(global_seqno_ == kDisableGlobalSequenceNumber);
|
|
|
|
key_ = raw_key_.GetUserKey();
|
|
|
|
key_pinned_ = raw_key_.IsKeyPinned();
|
|
|
|
} else if (global_seqno_ == kDisableGlobalSequenceNumber) {
|
|
|
|
key_ = raw_key_.GetInternalKey();
|
|
|
|
key_pinned_ = raw_key_.IsKeyPinned();
|
|
|
|
} else {
|
|
|
|
key_buf_.SetInternalKey(raw_key_.GetUserKey(), global_seqno_,
|
|
|
|
ExtractValueType(raw_key_.GetInternalKey()));
|
|
|
|
key_ = key_buf_.GetInternalKey();
|
|
|
|
key_pinned_ = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the result of `Comparator::Compare()`, where the appropriate
|
|
|
|
// comparator is used for the block contents, the LHS argument is the current
|
|
|
|
// key with global seqno applied, and the RHS argument is `other`.
|
|
|
|
int CompareCurrentKey(const Slice& other) {
|
|
|
|
if (raw_key_.IsUserKey()) {
|
|
|
|
assert(global_seqno_ == kDisableGlobalSequenceNumber);
|
|
|
|
return ucmp().Compare(raw_key_.GetUserKey(), other);
|
|
|
|
} else if (global_seqno_ == kDisableGlobalSequenceNumber) {
|
|
|
|
return icmp().Compare(raw_key_.GetInternalKey(), other);
|
|
|
|
}
|
|
|
|
return icmp().Compare(raw_key_.GetInternalKey(), global_seqno_, other,
|
|
|
|
kDisableGlobalSequenceNumber);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
const Comparator* raw_ucmp_;
|
|
|
|
// Store the cache handle, if the block is cached. We need this since the
|
|
|
|
// only other place the handle is stored is as an argument to the Cleanable
|
|
|
|
// function callback, which is hard to retrieve. When multiple value
|
|
|
|
// PinnableSlices reference the block, they need the cache handle in order
|
|
|
|
// to bump up the ref count
|
|
|
|
Cache::Handle* cache_handle_;
|
|
|
|
|
|
|
|
public:
|
|
|
|
// Return the offset in data_ just past the end of the current entry.
|
|
|
|
inline uint32_t NextEntryOffset() const {
|
|
|
|
// NOTE: We don't support blocks bigger than 2GB
|
|
|
|
return static_cast<uint32_t>((value_.data() + value_.size()) - data_);
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t GetRestartPoint(uint32_t index) {
|
|
|
|
assert(index < num_restarts_);
|
|
|
|
return DecodeFixed32(data_ + restarts_ + index * sizeof(uint32_t));
|
|
|
|
}
|
|
|
|
|
|
|
|
void SeekToRestartPoint(uint32_t index) {
|
|
|
|
raw_key_.Clear();
|
|
|
|
restart_index_ = index;
|
|
|
|
// current_ will be fixed by ParseNextKey();
|
|
|
|
|
|
|
|
// ParseNextKey() starts at the end of value_, so set value_ accordingly
|
|
|
|
uint32_t offset = GetRestartPoint(index);
|
|
|
|
value_ = Slice(data_ + offset, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
void CorruptionError();
|
|
|
|
|
|
|
|
protected:
|
|
|
|
template <typename DecodeKeyFunc>
|
|
|
|
inline bool BinarySeek(const Slice& target, uint32_t* index,
|
|
|
|
bool* is_index_key_result);
|
|
|
|
|
|
|
|
void FindKeyAfterBinarySeek(const Slice& target, uint32_t index,
|
|
|
|
bool is_index_key_result);
|
|
|
|
};
|
|
|
|
|
|
|
|
class DataBlockIter final : public BlockIter<Slice> {
|
|
|
|
public:
|
|
|
|
DataBlockIter()
|
|
|
|
: BlockIter(), read_amp_bitmap_(nullptr), last_bitmap_offset_(0) {}
|
|
|
|
DataBlockIter(const Comparator* raw_ucmp, const char* data, uint32_t restarts,
|
|
|
|
uint32_t num_restarts, SequenceNumber global_seqno,
|
|
|
|
BlockReadAmpBitmap* read_amp_bitmap, bool block_contents_pinned,
|
|
|
|
DataBlockHashIndex* data_block_hash_index)
|
|
|
|
: DataBlockIter() {
|
|
|
|
Initialize(raw_ucmp, data, restarts, num_restarts, global_seqno,
|
|
|
|
read_amp_bitmap, block_contents_pinned, data_block_hash_index);
|
|
|
|
}
|
|
|
|
void Initialize(const Comparator* raw_ucmp, const char* data,
|
|
|
|
uint32_t restarts, uint32_t num_restarts,
|
|
|
|
SequenceNumber global_seqno,
|
|
|
|
BlockReadAmpBitmap* read_amp_bitmap,
|
|
|
|
bool block_contents_pinned,
|
|
|
|
DataBlockHashIndex* data_block_hash_index) {
|
|
|
|
InitializeBase(raw_ucmp, data, restarts, num_restarts, global_seqno,
|
|
|
|
block_contents_pinned);
|
|
|
|
raw_key_.SetIsUserKey(false);
|
|
|
|
read_amp_bitmap_ = read_amp_bitmap;
|
|
|
|
last_bitmap_offset_ = current_ + 1;
|
|
|
|
data_block_hash_index_ = data_block_hash_index;
|
|
|
|
}
|
|
|
|
|
|
|
|
Slice value() const override {
|
|
|
|
assert(Valid());
|
|
|
|
if (read_amp_bitmap_ && current_ < restarts_ &&
|
|
|
|
current_ != last_bitmap_offset_) {
|
|
|
|
read_amp_bitmap_->Mark(current_ /* current entry offset */,
|
|
|
|
NextEntryOffset() - 1);
|
|
|
|
last_bitmap_offset_ = current_;
|
|
|
|
}
|
|
|
|
return value_;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline bool SeekForGet(const Slice& target) {
|
|
|
|
if (!data_block_hash_index_) {
|
|
|
|
SeekImpl(target);
|
|
|
|
UpdateKey();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
bool res = SeekForGetImpl(target);
|
|
|
|
UpdateKey();
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Invalidate(const Status& s) override {
|
|
|
|
BlockIter::Invalidate(s);
|
|
|
|
// Clear prev entries cache.
|
|
|
|
prev_entries_keys_buff_.clear();
|
|
|
|
prev_entries_.clear();
|
|
|
|
prev_entries_idx_ = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
friend Block;
|
|
|
|
inline bool ParseNextDataKey(bool* is_shared);
|
|
|
|
void SeekToFirstImpl() override;
|
|
|
|
void SeekToLastImpl() override;
|
|
|
|
void SeekImpl(const Slice& target) override;
|
|
|
|
void SeekForPrevImpl(const Slice& target) override;
|
|
|
|
void NextImpl() override;
|
|
|
|
void PrevImpl() override;
|
|
|
|
|
|
|
|
private:
|
|
|
|
// read-amp bitmap
|
|
|
|
BlockReadAmpBitmap* read_amp_bitmap_;
|
|
|
|
// last `current_` value we report to read-amp bitmp
|
|
|
|
mutable uint32_t last_bitmap_offset_;
|
|
|
|
struct CachedPrevEntry {
|
|
|
|
explicit CachedPrevEntry(uint32_t _offset, const char* _key_ptr,
|
|
|
|
size_t _key_offset, size_t _key_size, Slice _value)
|
|
|
|
: offset(_offset),
|
|
|
|
key_ptr(_key_ptr),
|
|
|
|
key_offset(_key_offset),
|
|
|
|
key_size(_key_size),
|
|
|
|
value(_value) {}
|
|
|
|
|
|
|
|
// offset of entry in block
|
|
|
|
uint32_t offset;
|
|
|
|
// Pointer to key data in block (nullptr if key is delta-encoded)
|
|
|
|
const char* key_ptr;
|
|
|
|
// offset of key in prev_entries_keys_buff_ (0 if key_ptr is not nullptr)
|
|
|
|
size_t key_offset;
|
|
|
|
// size of key
|
|
|
|
size_t key_size;
|
|
|
|
// value slice pointing to data in block
|
|
|
|
Slice value;
|
|
|
|
};
|
|
|
|
std::string prev_entries_keys_buff_;
|
|
|
|
std::vector<CachedPrevEntry> prev_entries_;
|
|
|
|
int32_t prev_entries_idx_ = -1;
|
|
|
|
|
|
|
|
DataBlockHashIndex* data_block_hash_index_;
|
|
|
|
|
|
|
|
bool SeekForGetImpl(const Slice& target);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Iterator over MetaBlocks. MetaBlocks are similar to Data Blocks and
|
|
|
|
// are used to store Properties associated with table.
|
|
|
|
// Meta blocks always store user keys (no sequence number) and always
|
|
|
|
// use the BytewiseComparator. Additionally, MetaBlock accesses are
|
|
|
|
// not recorded in the Statistics or for Read-Amplification.
|
|
|
|
class MetaBlockIter final : public BlockIter<Slice> {
|
|
|
|
public:
|
|
|
|
MetaBlockIter() : BlockIter() { raw_key_.SetIsUserKey(true); }
|
|
|
|
void Initialize(const char* data, uint32_t restarts, uint32_t num_restarts,
|
|
|
|
bool block_contents_pinned) {
|
|
|
|
// Initializes the iterator with a BytewiseComparator and
|
|
|
|
// the raw key being a user key.
|
|
|
|
InitializeBase(BytewiseComparator(), data, restarts, num_restarts,
|
|
|
|
kDisableGlobalSequenceNumber, block_contents_pinned);
|
|
|
|
raw_key_.SetIsUserKey(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Slice value() const override {
|
|
|
|
assert(Valid());
|
|
|
|
return value_;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
void SeekToFirstImpl() override;
|
|
|
|
void SeekToLastImpl() override;
|
|
|
|
void SeekImpl(const Slice& target) override;
|
|
|
|
void SeekForPrevImpl(const Slice& target) override;
|
|
|
|
void NextImpl() override;
|
|
|
|
void PrevImpl() override;
|
|
|
|
};
|
|
|
|
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
class IndexBlockIter final : public BlockIter<IndexValue> {
|
|
|
|
public:
|
|
|
|
IndexBlockIter() : BlockIter(), prefix_index_(nullptr) {}
|
|
|
|
|
|
|
|
// key_includes_seq, default true, means that the keys are in internal key
|
|
|
|
// format.
|
|
|
|
// value_is_full, default true, means that no delta encoding is
|
|
|
|
// applied to values.
|
|
|
|
void Initialize(const Comparator* raw_ucmp, const char* data,
|
|
|
|
uint32_t restarts, uint32_t num_restarts,
|
|
|
|
SequenceNumber global_seqno, BlockPrefixIndex* prefix_index,
|
|
|
|
bool have_first_key, bool key_includes_seq,
|
|
|
|
bool value_is_full, bool block_contents_pinned) {
|
|
|
|
InitializeBase(raw_ucmp, data, restarts, num_restarts,
|
|
|
|
kDisableGlobalSequenceNumber, block_contents_pinned);
|
|
|
|
raw_key_.SetIsUserKey(!key_includes_seq);
|
|
|
|
prefix_index_ = prefix_index;
|
|
|
|
value_delta_encoded_ = !value_is_full;
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
have_first_key_ = have_first_key;
|
|
|
|
if (have_first_key_ && global_seqno != kDisableGlobalSequenceNumber) {
|
|
|
|
global_seqno_state_.reset(new GlobalSeqnoState(global_seqno));
|
|
|
|
} else {
|
|
|
|
global_seqno_state_.reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Slice user_key() const override {
|
|
|
|
assert(Valid());
|
|
|
|
return raw_key_.GetUserKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
IndexValue value() const override {
|
|
|
|
assert(Valid());
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
if (value_delta_encoded_ || global_seqno_state_ != nullptr) {
|
|
|
|
return decoded_value_;
|
|
|
|
} else {
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
IndexValue entry;
|
|
|
|
Slice v = value_;
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
Status decode_s __attribute__((__unused__)) =
|
|
|
|
entry.DecodeFrom(&v, have_first_key_, nullptr);
|
|
|
|
assert(decode_s.ok());
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsValuePinned() const override {
|
|
|
|
return global_seqno_state_ != nullptr ? false : BlockIter::IsValuePinned();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
// IndexBlockIter follows a different contract for prefix iterator
|
|
|
|
// from data iterators.
|
|
|
|
// If prefix of the seek key `target` exists in the file, it must
|
|
|
|
// return the same result as total order seek.
|
|
|
|
// If the prefix of `target` doesn't exist in the file, it can either
|
|
|
|
// return the result of total order seek, or set both of Valid() = false
|
|
|
|
// and status() = NotFound().
|
|
|
|
void SeekImpl(const Slice& target) override;
|
|
|
|
|
|
|
|
void SeekForPrevImpl(const Slice&) override {
|
|
|
|
assert(false);
|
|
|
|
current_ = restarts_;
|
|
|
|
restart_index_ = num_restarts_;
|
|
|
|
status_ = Status::InvalidArgument(
|
|
|
|
"RocksDB internal error: should never call SeekForPrev() on index "
|
|
|
|
"blocks");
|
|
|
|
raw_key_.Clear();
|
|
|
|
value_.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PrevImpl() override;
|
|
|
|
|
|
|
|
void NextImpl() override;
|
|
|
|
|
|
|
|
void SeekToFirstImpl() override;
|
|
|
|
|
|
|
|
void SeekToLastImpl() override;
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
|
|
|
|
private:
|
|
|
|
bool value_delta_encoded_;
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
bool have_first_key_; // value includes first_internal_key
|
|
|
|
BlockPrefixIndex* prefix_index_;
|
|
|
|
// Whether the value is delta encoded. In that case the value is assumed to be
|
|
|
|
// BlockHandle. The first value in each restart interval is the full encoded
|
|
|
|
// BlockHandle; the restart of encoded size part of the BlockHandle. The
|
|
|
|
// offset of delta encoded BlockHandles is computed by adding the size of
|
|
|
|
// previous delta encoded values in the same restart interval to the offset of
|
|
|
|
// the first value in that restart interval.
|
Add an option to put first key of each sst block in the index (#5289)
Summary:
The first key is used to defer reading the data block until this file gets to the top of merging iterator's heap. For short range scans, most files never make it to the top of the heap, so this change can reduce read amplification by a lot sometimes.
Consider the following workload. There are a few data streams (we'll be calling them "logs"), each stream consisting of a sequence of blobs (we'll be calling them "records"). Each record is identified by log ID and a sequence number within the log. RocksDB key is concatenation of log ID and sequence number (big endian). Reads are mostly relatively short range scans, each within a single log. Writes are mostly sequential for each log, but writes to different logs are randomly interleaved. Compactions are disabled; instead, when we accumulate a few tens of sst files, we create a new column family and start writing to it.
So, a typical sst file consists of a few ranges of blocks, each range corresponding to one log ID (we use FlushBlockPolicy to cut blocks at log boundaries). A typical read would go like this. First, iterator Seek() reads one block from each sst file. Then a series of Next()s move through one sst file (since writes to each log are mostly sequential) until the subiterator reaches the end of this log in this sst file; then Next() switches to the next sst file and reads sequentially from that, and so on. Often a range scan will only return records from a small number of blocks in small number of sst files; in this case, the cost of initial Seek() reading one block from each file may be bigger than the cost of reading the actually useful blocks.
Neither iterate_upper_bound nor bloom filters can prevent reading one block from each file in Seek(). But this PR can: if the index contains first key from each block, we don't have to read the block until this block actually makes it to the top of merging iterator's heap, so for short range scans we won't read any blocks from most of the sst files.
This PR does the deferred block loading inside value() call. This is not ideal: there's no good way to report an IO error from inside value(). As discussed with siying offline, it would probably be better to change InternalIterator's interface to explicitly fetch deferred value and get status. I'll do it in a separate PR.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/5289
Differential Revision: D15256423
Pulled By: al13n321
fbshipit-source-id: 750e4c39ce88e8d41662f701cf6275d9388ba46a
5 years ago
|
|
|
IndexValue decoded_value_;
|
|
|
|
|
|
|
|
// When sequence number overwriting is enabled, this struct contains the seqno
|
|
|
|
// to overwrite with, and current first_internal_key with overwritten seqno.
|
|
|
|
// This is rarely used, so we put it behind a pointer and only allocate when
|
|
|
|
// needed.
|
|
|
|
struct GlobalSeqnoState {
|
|
|
|
// First internal key according to current index entry, but with sequence
|
|
|
|
// number overwritten to global_seqno.
|
|
|
|
IterKey first_internal_key;
|
|
|
|
SequenceNumber global_seqno;
|
|
|
|
|
|
|
|
explicit GlobalSeqnoState(SequenceNumber seqno) : global_seqno(seqno) {}
|
|
|
|
};
|
|
|
|
|
|
|
|
std::unique_ptr<GlobalSeqnoState> global_seqno_state_;
|
|
|
|
|
|
|
|
// Set *prefix_may_exist to false if no key possibly share the same prefix
|
|
|
|
// as `target`. If not set, the result position should be the same as total
|
|
|
|
// order Seek.
|
|
|
|
bool PrefixSeek(const Slice& target, uint32_t* index, bool* prefix_may_exist);
|
|
|
|
// Set *prefix_may_exist to false if no key can possibly share the same
|
|
|
|
// prefix as `target`. If not set, the result position should be the same
|
|
|
|
// as total order seek.
|
|
|
|
bool BinaryBlockIndexSeek(const Slice& target, uint32_t* block_ids,
|
|
|
|
uint32_t left, uint32_t right, uint32_t* index,
|
|
|
|
bool* prefix_may_exist);
|
|
|
|
inline int CompareBlockKey(uint32_t block_index, const Slice& target);
|
|
|
|
|
|
|
|
inline bool ParseNextIndexKey();
|
|
|
|
|
|
|
|
// When value_delta_encoded_ is enabled it decodes the value which is assumed
|
|
|
|
// to be BlockHandle and put it to decoded_value_
|
|
|
|
inline void DecodeCurrentValue(bool is_shared);
|
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|