【发布时间】:2021-05-04 13:27:58
【问题描述】:
我一直在试验一种随机写入工作负载,我使用多个线程写入 NVMe SSD 上一个或多个文件中的不相交偏移量。我使用的是 Linux 机器,写入是同步的,并且是使用直接 I/O 进行的(即,文件使用 O_DSYNC 和 O_DIRECT 打开)。
我注意到,如果线程同时写入单个文件,则实现的写入吞吐量不会随着线程数量的增加而增加(即,写入似乎是串行应用的,而不是并行应用的)。但是,如果每个线程都写入自己的文件,我确实会增加吞吐量(达到 SSD 制造商宣传的随机写入吞吐量)。请参阅下图了解我的吞吐量测量结果。
我想知道如果我有多个线程同时写入同一个文件中的非重叠区域,是否有人知道为什么我无法提高吞吐量?
以下是有关我的实验设置的一些其他详细信息。
我正在写入 2 GiB 的数据(随机写入)并改变用于写入的线程数(从 1 到 16)。每个线程一次写入 4 KiB 数据。我正在考虑两种设置:(1)所有线程都写入一个文件,(2)每个线程都写入自己的文件。在开始基准测试之前,使用的文件被打开并使用fallocate() 初始化为其最终大小。使用O_DIRECT 和O_DSYNC 打开文件。每个线程被分配一个随机不相交的文件内偏移子集(即,线程写入的区域不重叠)。然后,线程同时使用pwrite() 写入这些偏移量。
这是机器的规格:
- Linux 5.9.1-arch1-1
- 1 TB Intel NVMe SSD(型号 SSDPE2KX010T8)
- ext4 文件系统
- 128 GiB 内存
- 2.10 GHz 20 核 Xeon Gold 6230 CPU
SSD 应该能够提供高达 70000 IOPS 的随机写入。
我已经包含了一个独立的 C++ 程序,用于在我的机器上重现此行为。我一直在使用g++ -O3 -lpthread <file> 进行编译(我使用的是g++ 10.2.0 版)。
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
constexpr size_t kBlockSize = 4 * 1024;
constexpr size_t kDataSizeMiB = 2048;
constexpr size_t kDataSize = kDataSizeMiB * 1024 * 1024;
constexpr size_t kBlocksTotal = kDataSize / kBlockSize;
constexpr size_t kRngSeed = 42;
void AllocFiles(unsigned num_files, size_t blocks_per_file,
std::vector<int> &fds,
std::vector<std::vector<size_t>> &write_pos) {
std::mt19937 rng(kRngSeed);
for (unsigned i = 0; i < num_files; ++i) {
const std::string path = "f" + std::to_string(i);
fds.push_back(open(path.c_str(), O_CREAT | O_WRONLY | O_DIRECT | O_DSYNC,
S_IRUSR | S_IWUSR));
write_pos.emplace_back();
auto &file_offsets = write_pos.back();
int fd = fds.back();
for (size_t blk = 0; blk < blocks_per_file; ++blk) {
file_offsets.push_back(blk * kBlockSize);
}
fallocate(fd, /*mode=*/0, /*offset=*/0, blocks_per_file * kBlockSize);
std::shuffle(file_offsets.begin(), file_offsets.end(), rng);
}
}
void ThreadMain(int fd, const void *data, const std::vector<size_t> &write_pos,
size_t offset, size_t num_writes) {
for (size_t i = 0; i < num_writes; ++i) {
pwrite(fd, data, kBlockSize, write_pos[i + offset]);
}
}
int main(int argc, char *argv[]) {
assert(argc == 3);
unsigned num_threads = strtoul(argv[1], nullptr, 10);
unsigned files = strtoul(argv[2], nullptr, 10);
assert(num_threads % files == 0);
assert(num_threads >= files);
assert(kBlocksTotal % num_threads == 0);
void *data_buf;
posix_memalign(&data_buf, 512, kBlockSize);
*reinterpret_cast<uint64_t *>(data_buf) = 0xFFFFFFFFFFFFFFFF;
std::vector<int> fds;
std::vector<std::vector<size_t>> write_pos;
std::vector<std::thread> threads;
const size_t blocks_per_file = kBlocksTotal / files;
const unsigned threads_per_file = num_threads / files;
const unsigned writes_per_thread_per_file =
blocks_per_file / threads_per_file;
AllocFiles(files, blocks_per_file, fds, write_pos);
const auto begin = std::chrono::steady_clock::now();
for (unsigned thread_id = 0; thread_id < num_threads; ++thread_id) {
unsigned thread_file_offset = thread_id / files;
threads.emplace_back(
&ThreadMain, fds[thread_id % files], data_buf,
write_pos[thread_id % files],
/*offset=*/(thread_file_offset * writes_per_thread_per_file),
/*num_writes=*/writes_per_thread_per_file);
}
for (auto &thread : threads) {
thread.join();
}
const auto end = std::chrono::steady_clock::now();
for (const auto &fd : fds) {
close(fd);
}
std::cout << kDataSizeMiB /
std::chrono::duration_cast<std::chrono::duration<double>>(
end - begin)
.count()
<< std::endl;
free(data_buf);
return 0;
}
【问题讨论】:
标签: linux multithreading performance file-io io