RocksDB db_bench源码(一):random下单个thread的key生成方式与写入

原文链接:click here ,欢迎访问我的博客。

先说结论:

  • -num 指每个线程生成 key 的范围,-writes 指实际写入 kv 的数量,默认情况下 -writes=-1,指 writes==num。生成一条 kv 就写入一条 kv,所以实际上只生成了 writes 个 kv。
  • 每个线程随机都在 1~num 之间随机生成 key,基于 C++ 的随机数生成器 mt19937_64。每个线程用于随机的 seed 稍有不同:
    • seed_base + total_thread_count_ 为每个线程的最终 seed,其中前者都一样,但后者逐个相差 1,因为每设一个线程时该值会++。
    • 如果不指定 -seed(值为0),那么 seed_base 取自系统时间。
    • 如果指定 -seed,那么 seed_base 就是指定的值,不过一般不指定。
  • 如果指定了 -keys_per_prefix,即给 key 加前缀。那么每一个 key 的前缀仍然为随机数,随机prefix = (上述 key 的随机数 % 一个固定值)。

以最常见的 db_bench 配置为例进行分析,如下:

#!/bin/bash
sudo ../db_bench  \
--benchmarks="fillrandom,stats,levelstats" \
--max_background_jobs=4 \
--compression_type=none \
--num=1000000 \
--threads=4 \
--writes=250000\
--db=/home/nvme0/ysy/db_bench_test \
--wal_dir=/home/nvme0/ysy/db_bench_test \

db_bench_tool()

从入口 db_bench_tool() 开始分析,该函数用来解析命令行参数,首先由 ParseCommandLineFlags() 进行初步解析,相应参数放入对应的 FLAGS_xxx 中。

之后,会根据参数设置一些全局变量,seed_base 就在其中设定,代码如下:

// db_bench_tool()
if (!FLAGS_seed) {
    uint64_t now = FLAGS_env->GetSystemClock()->NowMicros();
    seed_base = static_cast<int64_t>(now);
    fprintf(stdout, "Set seed to %" PRIu64 " because --seed was 0\n",
            seed_base);
} else {
    seed_base = FLAGS_seed;
}

注意,此时还未进入多线程,且 seed_base 是全局变量,所以不管有没有指定 -seed,之后的每个线程的 seed_base 初值均一样。

该函数的其他部分暂时与 key 的生成无关了,直接进入 benchmark.Run()。

Run()

Run() 会首先调用 Open(),然后进一步调用 InitializeOptionsFromFlags(),其作用就是通过 FLAGS_xxx 初始化 options 中的相关字段。

接着,Run() 会进入一个很大的循环体,从 -benchmark 中选出参数逐个执行:

// Run()
while (std::getline(benchmark_stream, name, ',')) {
    // ...
}

拿本示例为例,fillrandom、stats、levelstats 会依次执行循环体。但是呢,后两者实际执行的内容仅仅是输出一些 DB 信息,所以这里不管,接下来的所有分析都只针对于 fillrandom。

首先会初始化一些变量,部分如下:

// Run()
num_ = FLAGS_num;
reads_ = (FLAGS_reads < 0 ? FLAGS_num : FLAGS_reads);
writes_ = (FLAGS_writes < 0 ? FLAGS_num : FLAGS_writes);
deletes_ = (FLAGS_deletes < 0 ? FLAGS_num : FLAGS_deletes);
value_size = FLAGS_value_size;
key_size_ = FLAGS_key_size;
entries_per_batch_ = FLAGS_batch_size;
writes_before_delete_range_ = FLAGS_writes_before_delete_range;
writes_per_range_tombstone_ = FLAGS_writes_per_range_tombstone;
range_tombstone_width_ = FLAGS_range_tombstone_width;
max_num_range_tombstones_ = FLAGS_max_num_range_tombstones;
// ...
int num_threads = FLAGS_threads;
int num_repeat = 1;
int num_warmup = 0;
// ...

主要注意以下变量:

  • num_:就是 -num
  • writes_ :如果指定 -writes,那么就是指定的值。如果没指定,FLAGS_writes==-1,所以此时 writes_ FLAGS_numnum_,符合官方声明

-writes (Number of write operations to do. If negative, do --num writes.)
type: int64 default: -1

  • entries_per_batch_:就是 --batch_size,指一个 WriteBatch 多少条 kv,默认为 1,一般不会另行指定
  • num_threads:就是 0-threads
  • num_repeat 和 num_warmup:这俩一直都是初值,1和0

接下来设置 fillrandom 的 method,为 WriteRandom,如下:

// Run()
else if (name == "fillrandom") {
    fresh_db = true;
    method = &Benchmark::WriteRandom;
}

之后会进行一些 DB 的初始化什么的,然后调用 RunBenchmark():

// Run()
for (int i = 0; i < num_repeat; i++) {
    Stats stats = RunBenchmark(num_threads, name, method);
    combined_stats.AddStats(stats);
    if (FLAGS_confidence_interval_only) {
        combined_stats.ReportWithConfidenceIntervals(name);
    } else {
        combined_stats.Report(name);
    }
}

// ....
Stats RunBenchmark(int n, Slice name,
                   void (Benchmark::*method)(ThreadState*))

num_repeat == 1,所以仅调用一次。其中 name==“fillrandom”,method=“WriteRandom”。

RunBenchmark()

进入 RunBenchmark(),从此处开始和多线程相关。其会给每个线程创建 ThreadArg 和 ThreadState,保存相关的状态,如下:

// RunBenchmark()
ThreadArg* arg = new ThreadArg[n];
for (int i = 0; i < n; i++) {
    arg[i].bm = this;
    arg[i].method = method;
    arg[i].shared = &shared;
    total_thread_count_++;
    arg[i].thread = new ThreadState(i, total_thread_count_);
    arg[i].thread->stats.SetReporterAgent(reporter_agent.get());
    arg[i].thread->shared = &shared;
    FLAGS_env->StartThread(ThreadBody, &arg[i]);
}

其中,核心为 arg[i].thread 的创建:

total_thread_count_++;
arg[i].thread = new ThreadState(i, total_thread_count_);

进入 Thread 的构造函数,如下:

// Per-thread state for concurrent executions of the same benchmark.
struct ThreadState {
    int tid;             // 0..n-1 when running in n threads
    Random64 rand;         // Has different seeds for different threads
    Stats stats;
    SharedState* shared;

    explicit ThreadState(int index, int my_seed)
        : tid(index), rand(seed_base + my_seed) {}
};

可以看到,核心就是 thread.rand,为 Random64 对象,它是一个随机数生成器,基于 C++ 的 mt19937_64 随机数生成器,完整代码如下:

class Random64 {
    private:
    std::mt19937_64 generator_;

    public:
    explicit Random64(uint64_t s) : generator_(s) { }

    // Generates the next random number
    uint64_t Next() { return generator_(); }

    // Returns a uniformly distributed value in the range [0..n-1]
    // REQUIRES: n > 0
    uint64_t Uniform(uint64_t n) {
        return std::uniform_int_distribution<uint64_t>(0, n - 1)(generator_);
    }

    // Randomly returns true ~"1/n" of the time, and false otherwise.
    // REQUIRES: n > 0
    bool OneIn(uint64_t n) { return Uniform(n) == 0; }

    // Skewed: pick "base" uniformly from range [0,max_log] and then
    // return "base" random bits.  The effect is to pick a number in the
    // range [0,2^max_log-1] with exponential bias towards smaller numbers.
    uint64_t Skewed(int max_log) {
        return Uniform(uint64_t(1) << Uniform(max_log + 1));
    }
};

因此,线程用于随机数生成的种子为 seed_base + total_thread_count_ 。而 total_thread_count_ 是随着循环递增的,所以每个线程的种子都不同,相互之间差 1。

接着,每个线程进入 ThreadBody(),并行执行 method。从此往后,所有的分析都基于独立的线程。

// ThreadBody()
thread->stats.Start(thread->tid);
(arg->bm->*(arg->method))(thread);

实际上就是 DoWrite(),指定写入类型为 RANDOM:

void WriteRandom(ThreadState* thread) {
    DoWrite(thread, RANDOM);
}

DoWrite()

进入 DoWrite 后,首先会定义一个变量 num_ops,值如下:

// DoWrite()
const int64_t num_ops = writes_ == 0 ? num_ : writes_;

很显然,它就是实际执行的写操作数量,除非指定 -writes=0,那么它就等于 writes_,在本例中就是 250000。

接下来,DoWrite() 会创建一个 vector<key_gen>,为 key 生成器,代码如下:

// DoWrite()
size_t num_key_gens = 1;
if (db_.db == nullptr) {
    num_key_gens = multi_dbs_.size();
}
printf("thread %ld 's num_key_gens is : %ld",thread->tid,num_key_gens)
    std::vector<std::unique_ptr<KeyGenerator>> key_gens(num_key_gens);
int64_t max_ops = num_ops * num_key_gens;
int64_t ops_per_stage = max_ops;
if (FLAGS_num_column_families > 1 && FLAGS_num_hot_column_families > 0) {
    ops_per_stage = (max_ops - 1) / (FLAGS_num_column_families /
                                     FLAGS_num_hot_column_families) +
        1;
}

Duration duration(test_duration, max_ops, ops_per_stage);
const uint64_t num_per_key_gen = num_ + max_num_range_tombstones_;
for (size_t i = 0; i < num_key_gens; i++) {
    key_gens[i].reset(new KeyGenerator(&(thread->rand), write_mode,
                                       num_per_key_gen, ops_per_stage));
}

其中,db_.db 一般不会为 nullptr,故 num_key_gens==1,即只有 1 个 key 生成器。如果不确定的话可以打日志看一下,本示例就是 1 。

max_ops 就是最大的写入次数,由于 num_key_gens==1,所以其值等于 num_ops,即250000。CF_num 默认就是 1,一般也不会指定,所以后面这个 if 体不用管。

num_per_key_gen 指一个 key_gen 要生成的 key 数量,如果不指定 -max_num_range_tombstones 选项的话,其值就是 num_,本例中为 1000000。

接下来就是创建 key_gen 了,其构造器如下:

class KeyGenerator {
    public:
    KeyGenerator(Random64* rand, WriteMode mode, uint64_t num,
                 uint64_t /*num_per_set*/ = 64 * 1024)
        : rand_(rand), mode_(mode), num_(num), next_(0) {
            if (mode_ == UNIQUE_RANDOM) {
                values_.resize(num_);
                for (uint64_t i = 0; i < num_; ++i) {
                    values_[i] = i;
                }
                RandomShuffle(values_.begin(), values_.end(),
                              static_cast<uint32_t>(seed_base));
            }
        }
}

因为 mode==RANDOM,所以函数体都没用,仅仅赋值了成员变量,核心就是 rand,也就是上文的 thread->rand。

生成完 key 的构造器后,开始生成 value 的构造器:

RandomGenerator gen;

这个构造器和 key_gen 不同,它主要依据 Distribution 来决定 value,分为 uniform、normal、fixed,不过我们这里只讨论 key 的生成,所以该构造器不详细说明。

接着,DoWrite() 会定义一个 batch,并给 key 分配空间,相关代码如下:

// DoWrite()
WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
                 FLAGS_write_batch_protection_bytes_per_key,
                 user_timestamp_size_);
Status s;
int64_t bytes = 0;

std::unique_ptr<const char[]> key_guard;
Slice key = AllocateKey(&key_guard);
std::unique_ptr<const char[]> begin_key_guard;
Slice begin_key = AllocateKey(&begin_key_guard);
std::unique_ptr<const char[]> end_key_guard;
Slice end_key = AllocateKey(&end_key_guard);

后面有一长段代码是关于 disposable/persistent keys simulation 的,如下:

// DoWrite()
bool skip_for_loop = false, is_disposable_entry = true;
std::vector<uint64_t> disposable_entries_index(num_key_gens, 0);
std::vector<uint64_t> persistent_ent_and_del_index(num_key_gens, 0);
printf("thread %d 's disposable_entries_batch_size is : %ld\n",thread->tid, FLAGS_disposable_entries_batch_size);
printf("thread %d 's persistent_entries_batch_size is : %ld\n",thread->tid, FLAGS_persistent_entries_batch_size);
const uint64_t kNumDispAndPersEntries =
    FLAGS_disposable_entries_batch_size +
    FLAGS_persistent_entries_batch_size;

官方对其说明如下:

— Variables used in disposable/persistent keys simulation:

The following variables are used when disposable_entries_batch_size is >0. We simualte a workload where the following sequence is repeated multiple times: “A set of keys S1 is inserted (‘disposable entries’), then after some delay another set of keys S2 is inserted (‘persistent entries’) and the first set of keys S1 is deleted. S2 artificially represents the insertion of hypothetical results from some undefined computation done on the first set of keys S1. The next sequence can start as soon as the last disposable entry in the set S1 of this sequence is inserted, if the delay is non negligible”

实际上我们一般不会用这个,至少在本例中,FLAGS_disposable_entries_batch_size 和 FLAGS_persistent_entries_batch_size 均为 0,故 kNumDispAndPersEntries,所以后续所有要求 (kNumDispAndPersEntries > 0)的操作均不用看。

在进入真正的写入循环之前,DoWrite() 定义了一些变量:

// DoWrite()
int64_t stage = 0;
int64_t num_written = 0;
int64_t next_seq_db_at = num_ops;
size_t id = 0;
int64_t num_range_deletions = 0;

着重关注两个变量:

  • num_written:已经写入的 kv 数量。
  • id:key_gen 的 id,即 vector<key_gen> 的元素下标。

接着,DoWrite() 将正式进入写循环,该循环有两层,如下:

// DoWrite()
while ((num_per_key_gen != 0) && !duration.Done(entries_per_batch_)) {
    // ...
    for (int64_t j = 0; j < entries_per_batch_; j++) {
        // ...
    }
}

其中,外层用来判断是否所有的写操作都已结束,内层用来把每一条写入 Put 进 batch 中。首先看外层,循环的结束标志为 duration.Done() 返回 true,其函数体如下:

bool Done(int64_t increment) {
    if (increment <= 0) increment = 1;    // avoid Done(0) and infinite loops
    ops_ += increment;

    if (max_seconds_) {
        // Recheck every appx 1000 ops (exact iff increment is factor of 1000)
        auto granularity = FLAGS_ops_between_duration_checks;
        if ((ops_ / granularity) != ((ops_ - increment) / granularity)) {
            uint64_t now = FLAGS_env->NowMicros();
            return ((now - start_at_) / 1000000) >= max_seconds_;
        } else {
            return false;
        }
    } else {
        return ops_ > max_ops_;
    }
}

很简单,每写完一个 batch,ops_ 都会加 increment,后者就是 batch 的 kv 数,一旦达到了 max_ops_ ,就返回 true。因此,外层循环的结束标志是,writes_ 个 kv 全部写完。

大循环首先会确定 key_gen 的 id,如下:

// DoWrite()
while ((num_per_key_gen != 0) && !duration.Done(entries_per_batch_)) {
    if (write_mode != SEQUENTIAL) {
        id = thread->rand.Next() % num_key_gens;
      }
    // ...
}

由于 num_key_gens==1,所以 id 始终为 0。

直接看内层循环,它会先生成一个 key,如下:

for (int64_t j = 0; j < entries_per_batch_; j++) {
    int64_t rand_num = 0;
    if ((write_mode == UNIQUE_RANDOM) && (p > 0.0)) {
        // ... 跳过
    }
    else if (kNumDispAndPersEntries > 0) {
        // ... 跳过
    }
    else {
          rand_num = key_gens[id]->Next();
    }
    GenerateKeyFromInt(rand_num, FLAGS_num, &key);
}

其中,Next() 代码如下:

class KeyGenerator {
    uint64_t Next() {
        switch (mode_) {
            case SEQUENTIAL:
                return next_++;
            case RANDOM:
                return rand_->Next() % num_;
            case UNIQUE_RANDOM:
                assert(next_ < num_);
                return values_[next_++];
        }
        assert(false);
        return std::numeric_limits<uint64_t>::max();
    }
}

可以看到,在 Random 模式下,通过 Random64(也就是 mt19937_64)来生成一个随机数然后取模来保证范围。

GenerateKeyFromInt() 将用该随机数生成 key,部分代码如下:

void GenerateKeyFromInt(uint64_t v, int64_t num_keys, Slice* key) {
    char* start = const_cast<char*>(key->data());
    char* pos = start;
    if (keys_per_prefix_ > 0) {
        int64_t num_prefix = num_keys / keys_per_prefix_;
        int64_t prefix = v % num_prefix;
        int bytes_to_fill = std::min(prefix_size_, 8);
        if (port::kLittleEndian) {
            for (int i = 0; i < bytes_to_fill; ++i) {
                pos[i] = (prefix >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
            }
        } else {
            memcpy(pos, static_cast<void*>(&prefix), bytes_to_fill);
        }
        if (prefix_size_ > 8) {
            // fill the rest with 0s
            memset(pos + 8, '0', prefix_size_ - 8);
        }
        pos += prefix_size_;
    }

    int bytes_to_fill = std::min(key_size_ - static_cast<int>(pos - start), 8);
    if (port::kLittleEndian) {
        for (int i = 0; i < bytes_to_fill; ++i) {
            pos[i] = (v >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
        }
    } else {
        memcpy(pos, static_cast<void*>(&v), bytes_to_fill);
    }
    pos += bytes_to_fill;
    if (key_size_ > pos - start) {
        memset(pos, '0', key_size_ - (pos - start));
    }
}

分两部分来看,存在 prefix 和不存在 prefix。当指定了 -keys_per_perfix 后,将通过刚才的随机数生成一个前缀,然后把它插入 key 之前:

// GenerateKeyFromInt()
int64_t num_prefix = num_keys / keys_per_prefix_;
int64_t prefix = v % num_prefix;
// ...
memcpy(pos, static_cast<void*>(&prefix), bytes_to_fill);

如果不存在 prefix,那么就是单纯把随机数放进去,然后不足补0:

if (port::kLittleEndian) {
    for (int i = 0; i < bytes_to_fill; ++i) {
        pos[i] = (v >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
    }
} else {
    memcpy(pos, static_cast<void*>(&v), bytes_to_fill);
}
pos += bytes_to_fill;
if (key_size_ > pos - start) {
    memset(pos, '0', key_size_ - (pos - start));
}

回到 DoWrite(),生成完 key 后,开始生成 value,如下:

// DoWrite()
Slice val;
if (kNumDispAndPersEntries > 0) {
    // 跳过
} else {
    val = gen.Generate();
}

因为 FLAGS_num_column_families==1,所以进入如下代码块,直接将 key-value 插入 batch。

// DoWrite()
else if (FLAGS_num_column_families <= 1) {
    batch.Put(key, val);
}

插入完成后,num_written 自增,不过注意此时 batch 还未写入 DB。

// DoWrite()
++num_written;

-use_blob_db 指定是否使用 BlobDB,默认为 false,不使用。所以一路来到如下代码块:

// DoWrite()
if (!use_blob_db_) {
    // Not stacked BlobDB
    s = db_with_cfh->db->Write(write_options_, &batch);
}

从这里开始就进入了 RocksDB 的 Write 入口了,接下来的操作就如 RocksDB源码:写 系列中所述了。至此,一个 bacth 中 kv 的写入基本就结束了,然后回到外层循环,通过 duration.Done() 判断是否继续。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值