如何在Linux中提高SSD I/O吞吐量并发性

4
下面的程序从文件中读取一堆行并解析它们。它可能会更快。另一方面,如果我有多个核心和多个要处理的文件,那么这就不太重要了;我可以并行运行作业。
不幸的是,在我的 arch 机器上似乎无法正常工作。运行两个程序副本仅比运行一个副本稍微快一些(如果有的话),而且不到我的驱动器能力的20%。在具有相同硬件的 ubuntu 机器上,情况略有改善。我获得3-4个内核的线性扩展,但我仍然达到了SSD驱动器容量的约50%。
是什么障碍阻止了I/O吞吐量随着内核数量的增加而呈线性扩展,并且在软件/操作系统方面如何提高I/O并发性?
附言 - 对于下面提到的硬件,单个内核足够快,如果我将解析移到单独的线程中,则读取将成为I/O限制。还有其他优化可用于提高单核性能。但是,对于这个问题,我想专注于并发性以及我的编码和操作系统选择如何影响它。

细节:

这里是几行iostat -x 1的输出:

使用dd将文件复制到/dev/null:

Device:         rrqm/s   wrqm/s     r/s     w/s     rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               0.00     0.00  883.00    0.00 113024.00     0.00   256.00     1.80    2.04    2.04    0.00   1.13 100.00

运行我的程序:
Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               1.00     1.00  141.00    2.00 18176.00    12.00   254.38     0.17    1.08    0.71   27.00   0.96  13.70

同时运行两个程序实例,读取不同的文件:
Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda              11.00     0.00  139.00    0.00 19200.00     0.00   276.26     1.16    8.16    8.16    0.00   6.96  96.70

我只能说这个效果微乎其微!增加核心数量并不能提高吞吐量,事实上它开始恶化并变得不那么一致。

这里有一个我的程序实例和一个dd实例:

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sda               9.00     0.00  468.00    0.00 61056.00     0.00   260.92     2.07    4.37    4.37    0.00   2.14 100.00

这是我的代码:

#include <string>

#include <boost/filesystem/path.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/fstream.hpp>

typedef boost::filesystem::path path;
typedef boost::filesystem::ifstream ifstream;

int main(int argc, char ** argv) {
  path p{std::string(argv[1])};
  ifstream f(p);
  std::string line;
  std::vector<boost::iterator_range<std::string::iterator>> fields;

  for (getline(f,line); !f.eof(); getline(f,line)) {
    boost::split (fields, line, boost::is_any_of (","));
  }
  f.close();
  return 0;
}

这是我编译它的步骤:

g++ -std=c++14 -lboost_filesystem -o gah.o -c gah.cxx
g++ -std=c++14 -lboost_filesystem -lboost_system -lboost_iostreams -o gah gah.o

编辑:更多细节

在运行上述基准测试之前,我清除内存缓存(释放页面缓存、目录项和索引节点),以避免Linux从缓存中拉取页面。

我的进程似乎受到CPU限制;切换到mmap或通过pubsetbuf更改缓冲区大小对记录的吞吐量没有明显影响。

另一方面,扩展性受到IO限制。如果我在运行程序之前将所有文件都带入内存缓存中,则吞吐量(现在通过执行时间来测量,因为iostat无法查看它)与核心数成线性比例增长。

我真正想了解的是,当我使用多个顺序读取进程从磁盘读取时,为什么吞吐量不会随着进程数量线性增长,直到接近驱动器的最大读取速度?为什么我会达到I/O限制而没有饱和吞吐量,以及我何时达到这种状态取决于我运行的操作系统/软件堆栈?


1
你可能遇到了基于I/O大小的差异。我建议尝试使用不同的读取缓冲区大小进行实验。请参考https://dev59.com/kW435IYBdhLWcg3w3EIi和http://en.cppreference.com/w/cpp/io/basic_filebuf/setbuf。你甚至可以尝试使用原始的C风格系统调用`open()`和`read()`进行基准测试,通过命令行参数传递不同的缓冲区大小(和inter-`read()`延迟时间)。 - Andrew Henle
2个回答

2

您没有在比较相似的事物。

您正在比较:

Copying a file to /dev/dull with dd:

(我假定你指的是/dev/null……)
使用
int main(int argc, char ** argv) {
  path p{std::string(argv[1])};
  ifstream f(p);
  std::string line;
  std::vector<boost::iterator_range<std::string::iterator>> fields;

  for (getline(f,line); !f.eof(); getline(f,line)) {
    boost::split (fields, line, boost::is_any_of (","));
  }
  f.close();
  return 0;
}

第一种方法只是读取原始字节,不关心它们是什么,然后将它们倾倒到位桶中。你的代码按行读取,需要进行识别,然后拆分为向量。

以你的方式读取数据,你先读取一行,然后花时间处理它。与你的代码进行比较的dd命令从不花时间做其他事情,而是直接读取数据 - 它不必读取、处理、再读取、再处理...


感谢指出笔误!使用dd只是为了说明驱动器的性能。我可以通过将boost::split行替换为count++(以防止循环被优化掉),或者使用-O2编译来获得与dd非常相似的性能。我的最小工作示例被设计得足够慢,不会受到I / O限制;我必须使其执行某些操作才能实现这一点。 - Tobias Hagge
1
@TobiasHagge 你是在尝试让你的I/O扩展吗?这样你就可以通过运行两个进程副本获得2倍的性能,通过运行三个进程副本获得3倍的性能? - Andrew Henle

1
我认为这里至少存在三个问题:
1)我的读取太频繁了。
我从文件中读取的行长度是可预测的,分隔符也是可预测的。通过每千次随机引入一微秒的延迟,我能够将多个核心的吞吐量提高到约45MB/s。
2)我的pubsetbuf实现实际上没有设置缓冲区大小。
标准仅规定当指定缓冲区大小为零时,pubsetbuf会关闭缓冲,如this link所述(感谢@Andrew Henle)。所有其他行为都是实现定义的。显然,我的实现使用了8191字节的缓冲区大小(由strace验证),无论我设置什么值。
因为懒得为测试目的实现自己的流缓冲,我将代码重写为在第二个循环中读取1000行数据到向量中,然后尝试解析它们,然后重复整个过程直到文件结尾(没有随机延迟)。这使我能够扩展到约50MB/s。
3)我的I/O调度程序和设置不适合我的驱动器和应用程序。
显然,Arch Linux默认使用适用于HDD驱动器的参数的cfq io调度程序来使用我的SSD驱动器。根据这里所述(请参见Mikko Rantalainen的回答和链接的文章),将slice_sync设置为0,或者切换到noop调度程序,如这里所述,原始代码最大吞吐量约为60MB/s,运行四个内核。这个链接也很有帮助。
使用noop调度,扩展似乎几乎是线性的,直到我的机器的四个物理内核(我有八个超线程)。

通过在千分之一的时间内随机引入1微秒的延迟,我能够将多个核心之间的吞吐量推高到约45MB/s。好吧,这很奇怪。我很想弄清楚那是如何发生的。 - Andrew Henle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接