在单个ThreadRipper工作站上实现11M IOPS和66 GB / S IO

2021-01-29 22:34:37

TL; DR现代磁盘是如此之快,以至于系统性能瓶颈转移到了RAM访问和CPU上。拥有多达64个内核,PCIe 4.0和8个内存通道,即使是单插槽的AMD ThreadRipper Pro工作站也能使一台功能强大的机器陷入困境-如果您操作正确的话!

在这篇文章中,我将说明如何配置具有10个PCIe 4.0 SSD的AMD ThreadRipper Pro工作站,以实现1100万IOPS,4kB随机读取和66 Gb / s吞吐率以及更大的IO,以及哪些瓶颈和问题。我解决的问题。我们将研究Linux块I / O内部及其与现代硬件的交互。我们将使用工具&衡量瓶颈的新旧技术,以及内核I / O堆栈中的其他功能。

$ dstat -pcmrd --- procs --- ---- total-usage ---- ------ memory-usage ----- --io / total- -dsk / total-run blk new | usr sys idl wai stl |用过的免费buf cach |阅读命令|读命令32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 33 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 33 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.0M 0 | 42G 0 32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.0M 0 | 42G 0 32 0 0 | 28 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 32 0 0 | 27 72 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0 32 0 0 | 27 73 0 0 0 | 2232M 249G 61M 568M | 11.1M 0 | 42G 0

使用4kB块大小的随机读取,我获得了超过1100万次IOPS,吞吐量为42 GiB /(〜45 GB / s)。我将在下面详细介绍技术细节和较大的块,但首先要回答“为什么”问题。

为什么甚至在一台计算机上都需要这样的IO吞吐量?我是否应该“为了可扩展性”在云中构建50个节点的集群?这正是我实验的重点-如果您可以仅在具有本地NVMe存储的一台服务器上运行I / O繁重的工作,您是否真的想拥有集群的所有复杂性或远程存储的性能影响?多少个数据库甚至需要维持“仅” 1M磁盘IOPS?或者,如果您确实确实需要1 TB / s的数据扫描速度,则可以使用10-20个配置良好的群集节点(而不是200个)来完成此任务。如果使用得当,现代硬件功能强大!

我非常了解各种企业数据管理功能需求,例如可用性,远程复制,数据共享或庞大的数据量,它们可能会导致您不使用本地NVMe SSD作为主要存储,因此我对此并不反对。其中一些要求可以通过软件解决,而某些则不能。本文的目的是显示当今廉价的日用品硬件所能提供的原始性能,因此,如果您的公司为提高100倍的吞吐量而支付10倍的费用,那么最好知道其他选择。

我对本系列文章的未来计划包括使用各种数据库引擎运行I / O重性能测试,也欢迎其他想法!

CPU核心的最大数量为64(128个线程),但我购买了16核版本,因为我的价格便宜

所有16个内核都可以3.9 GHz持续基频运行,最大“升压”频率为4.3 GHz

因此,该工作站的出色吞吐量不仅取决于CPU的处理速度,还取决于8个存储通道和128个PCIe 4.0通道所提供的额外带宽!单个PCIe 4.0通道在每个方向上为您提供大约1.969 GB / s的带宽(PCIe是交换式的,点对点全双工)。从理论上讲,如果所有通道都被充分使用,则128条通道应该意味着CPU可以在每个方向上处理〜250 GB / s(2 TB / s!)PCIe流量。

请注意,在现代CPU上,大多数PCIe通道都直接连接到CPU(与内存通道一样),并且不通过某些外部“南桥”控制器或“前端总线”。

这也意味着单个PCIe 4.0 x4卡可以达到接近8 GB / s的数据传输速度,前提是设备可以处理它并且您不会首先遇到其他瓶颈。这将引导我们进入下一部分,合适的SSD。

三星980 Pro是一款真正的PCIe 4.0 SSD,其规格声称读取速度为7000 MB / s,写入吞吐量为5000 MB / s(带有警告)。它的内部控制器能够实现真正的PCIe 4.0传输速度,而不是旧的PCIe 3.0芯片,而只是将其表示为具有低GT / s的PCIe 4.0兼容芯片。我知道其他一些“ PCIe4” SSD将以PCIe3速度(〜3.5 GB / s)达到最大速度,因为它们中没有新一代的控制器。

我最终购买了8 x 1 TB SSD,最终用于数据量,而2 x 500 GB用来引导磁盘和软件。我也有一个380 GB的Intel Optane 905P SSD,用于低延迟写入(例如事务日志),但在以后的文章中会详细介绍。

$ sudo nvme listNode SN模型---------------- -------------------- -------- ------------------- / dev / nvme0n1 S5P2NG0N902798J Samsung SSD 980 PRO 1TB / dev / nvme1n1 S5P2NG0NA02399T Samsung SSD 980 PRO 1TB / dev / nvme2n1 S5P2NG0NA04362H Samsung SSD 980 PRO 1TB / dev / nvme3n1 S5P2NG0N902802P三星SSD 980 PRO 1TB / dev / nvme4n1 S5P2NG0NA00551P三星SSD 980 PRO 1TB / dev / nvme5n1 S5P2NG0NA03266N三星SSD 980 PRO 1TB / dev / nvme6n1 S5P2NG0NA01498X dev / nvme8n1 S5NYNG0N906374T Samsung SSD 980 PRO 500GB / dev / nvme9n1 S5NYNG0N906379K Samsung SSD 980 PRO 500GB

三星的规格页面(1,2)指出最大连续读取&写入速度为7000 MB / s使用PCIe 4.0的1 TB驱动器为5000 MB / s。 IO模式实际上不必是顺序的,只需具有足够大的I / O大小即可。将这些卡插入PCIe 3.0插槽后,由于PCIe 3.0 x4吞吐量的限制,您的读写速度仅为3500 MB / s。

像TLC和amp; amp;一样,采用一粒盐的写入速度。 QLC卡对主NAND区域的多位写入速度较慢,但​​可能有一些DIMM内存用于缓冲写入和/或“ TurboWrite缓冲区”(如Samsung所说),它使用SSD NAND的一部分作为较快的SLC存储。通过向TLC区域发出一位“类似于SLC”的写入来完成。因此,一旦以5000 MB / s的速度填充了“ SLC” TurboWrite缓冲区,TLC“主区域”将以2000 MB / s(在1 TB磁盘上)出现瓶颈。

显然在980年代,TurboWrite缓冲区在1 TB SSD上默认为〜6 GB,但它是动态的,如果有大量的写入需求和足够的未使用NAND空间,它可以增长到108 GB。三星还拥有自己的内部磁盘控制器(Elpis),该磁盘控制器旨在实现PCIe 4.0速度。它可以处理128个I / O队列(每个队列有64k命令集!)。本文是有关此磁盘的很好参考-并概述了现代SSD的复杂性(和潜在的瓶颈)!

因为我只想关注本文的最大原始块I / O性能,所以我将直接针对NVMe块设备(在I / O路径上没有文件系统或LVM)运行测试。 NVMe块设备默认情况下启用了按CPU的多队列(MQ),并且设备中断在所有CPU上“分段”。我将在以后的文章中介绍一些LVM,多队列和文件系统测试。

在这些测试中,我使用了Ubuntu 20.10和Ubuntu提供的Linux内核5.8.0-29-generic。我也简短地测试了(当前)最新的Linux内核5.11-rc4,它具有一些io_uring增强功能,但是吞吐量较低。其中有一些大型的AMD ThreadRipper(可感知功耗的调度程序)更新,显然有一些未解决的性能下降。

我首先进行了单磁盘测试,以避免在同时访问所有10个SSD时遇到任何系统范围的吞吐量瓶颈。该测试旨在为我提供单个磁盘的理论最大吞吐量。我在--io_uring异步I / O选项中使用了fio。不出所料,它比libaio更有效。

我最终在狭窄的综合基准测试中使用了以下fio命令。在本文的后面,我将解释某些命令行选项的原因。

#!/ bin / bash [$#-ne 3]&& echo用法$ 0 numjobs / dev / DEVICENAME BLOCKSIZE&&退出1fio --readonly --name = onessd \ --filename = $ 2 \ --filesize = 100g --rw = randread --bs = $ 3 --direct = 1 --overwrite = 0 \ --numjobs = $ 1- iodepth = 32 --time_based = 1 --runtime = 3600 \ --ioengine = io_uring \ --registerfiles --fixedbufs \ --gtod_reduce = 1 --group_reporting

好的,让我们首先在3个并发工作线程进行4kB读取的情况下,仅对单个磁盘进行读取(每个工作线程的queue_depth = 32),以查看机器中此类磁盘的最大理论吞吐量:

看起来我们从磁盘中获得的IOPS比三星承诺的还要多-他们的规格说“每个磁盘”仅1,000,000 IOPS。如果您想知道为什么我要使用3个并发的辅助进程来运行单磁盘测试,那么事实证明,在我的设置中,单个进程使单个CPU的最大运行速度为450k IOPS。因此,单个进程实际上无法在每秒CPU时间(每秒100%在CPU上)上提交(和获取)超过450k I / O。假设它的CPU核心运行在3.9 GHz左右(并且没有其他事情要做),那么它将转化为大约3,900,000,000 / 450,000 = 8,666个CPU周期,用于提交+发行+完成+获得每个I / O。

如果查看下面的指标,您会发现我的3个工作进程使用了​​32个(逻辑)处理器的计算机的CPU容量约占9-10%:

$ dstat -pcrmd --- procs --- ---- total-usage ---- ------ memory-usage ----- --io / total- -dsk / total-run blk new | usr sys idl wai stl |用过的免费buf cach |阅读命令|读命令3.0 0 0 | 2 8 91 0 0 | 1792M 249G 41M 406M | 1145k 0 | 4472M 0 3.0 0 0 | 2 7 91 0 0 | 1793M 249G 41M 406M | 1150k 0 | 4493M 0 3.0 0 0 | 2 8 91 0 0 | 1793M 249G 41M 406M | 1152k 0 | 4499M 0 3.0 0 0 | 1 8 91 0 0 | 1793M 249G 41M 406M | 1151k 0 | 4498M 0

旁注:Ubuntu 20.10上的dstat看起来有一些CPU利用率四舍五入报告错误。我使用其他工具来验证本文中的CPU编号是否相似。

大型I / O呢?让我们尝试1 MB大小的读取,这是数据库引擎将用于扫描大型表的读取:

似乎我们实际上无法进行1 MB大小的读取,因为我们在下面发布了约6.800 MB的1.38万个读取操作。 Fio不能发出1 MB大小的IO,或者更大的请求被分成了内核中某处的〜512kB块:

---- total-usage ---- --- procs --- ------ memory-usage ----- --io / total- -dsk / total-usr sys idl wai stl | run blk新|用过的免费buf cach |阅读命令|读命令0 1 99 0 0 | 0 0 0 | 1990M 249G 54M 441M | 13.8k 0 | 6807M 0 0 1 99 0 0 | 0 0 0 | 1990M 249G 54M 441M | 13.7k 0 | 6799M 0 0 1 99 0 0 | 0 0 0 | 1990M 249G 54M 441M | 13.8k 0 | 6805M 0 0 1 99 0 0 | 0 0 0 | 1990M 249G 54M 441M | 13.7k 0 | 6803M 0

由于I / O大小较大,每个请求都需要花费大量时间,因为磁盘的DMA控制器会将请求的数据复制到RAM中的相关内存位置。 PCIe 4.0 x4的最大理论传输速率约为7.877 GB / s,即使是从磁盘控制器内存到CPU的PCIe传输,也需要超过120 us / MB,除了闪存读取和SSD控制器的延迟。

因此,从上面的CPU使用率数据可以看出,尽管我运行3个并发的fio工作程序,但我的CPU还是空闲了99%,并且top确认了这一点。工人流程当时在做什么?他们大部分时间都在睡觉,等待W_io_cqring_wait在io_uring完成队列中的事件:

$ sudo psn -G syscall,wchan -a -p ^ fioLinux进程快照程序v0.18,由Tanel Poder [https://0x.tools]采样/ proc / syscall,wchan,统计5秒钟...完成。== =活动线程=============================================== ============================样本| avg_threads |通讯|州| syscall | chan ------------------------------------------------- ----------------------------------------------- 203 | 2.03 | (fio)|睡眠(可中断)| io_uring_enter | io_cqring_wait 100 | 1.00 | (fio)|睡眠(可中断)|选择| do_select 85 | 0.85 | (fio)|睡眠(可中断)| clock_nanosleep | hrtimer_nanosleep 14 | 0.14 | (fio)|磁盘(不间断)| openat | __blkdev_get 12 | 0.12 | (fio)|正在运行(在CPU上)| io_uring_enter | 0 12 | 0.12 | (fio)|睡眠(可中断)| [运行中] io_cqring_wait 9 | 0.09 | (fio)|正在运行(在CPU上)| io_uring_enter | io_cqring_wait 2 | 0.02 | (fio)|正在运行(在CPU上)| [运行中] 0 2 | 0.02 | (fio)|睡眠(可中断)| [运行中] 0 1 | 0.01 | (fio)|正在运行(在CPU上)| futex | futex_wait_queue_me样本:100(预期:100)总进程:4,线程:5运行时:5.00,测量时间:0.18

在以下两节中,我将展示如何尝试不同的OS级别I / O配置选项(直接或缓存I / O以及使用I / O调度程序)。这些部分深入探讨了Linux内核故障排除主题,如果您想跳过本主题并阅读有关硬件配置的挑战,请跳至“多磁盘测试”部分。

我使用--direct = 1选项强制使用O_DIRECT标志打开文件-绕开了OS页面缓存。当运行数百万个IOPS时,您希望将每个操作以及复制,搜索和复制的CPU开销降到最低。替换操作系统页面缓存中的页面将从根本上增加您的CPU使用率和内存流量。无论如何,大多数成熟的数据库引擎都有内置的缓存,因此为什么要重复工作(和使用内存)。

但是为了好玩,我还是使用--direct = 0进行了相同的测试,结果如下:

$ dstat -pcmrd --- procs --- ---- total-usage ---- ------ memory-usage ----- --io / total- -dsk / total-run blk new | usr sys idl wai stl |用过的免费buf cach |阅读命令|读取命令75 0 0 | 0 100 0 0 0 | 1997M 1864M 244G 243M | 5947 0 | 2953M 0 60 3.0 0 | 0 100 0 0 0 | 2005M 1193M 245G 243M | 7188 11.0 | 3593M 244k 56 2.0 0 | 0 99 0 1 0 | 1993M 1363M 245G 243M | 6290 0 | 3143M 0 56 1.0 0 | 0 99 0 1 0 | 1992M 1316M 245G 240M | 6545 0 | 3266M 0 38 19 0 | 0 99 0 1 0 | 1984M 1271M 245G 237M | 6493 0 | 3239M 0 ^ C

等一下我的由3名工作人员组成的fio测试以某种方式使所有CPU在内核模式下保持100%繁忙?可运行线程(运行列)在38和75之间交替显示?读取吞吐量已从6 GiB / s以上降至3 GiB / s。但是为什么我们有这么多的CPU活动?就是这样吗?

我们不要猜测或放弃,而要衡量!由于内核CPU使用率很高,我们提供了两个可供选择的选项。

$ sudo psn -G syscall,wchanLinux进程快照程序v0.18,由Tanel Poder [https://0x.tools]采样/ proc / syscall,wchan,stat持续5秒钟...已完成。===活动线程=== ================================================== ======================================样品avg_threads |通讯|州| syscall | chan ------------------------------------------------- -------------------------------------------------- ---------------- 4218 | 59.41 | (io_wqe_worker- *)|磁盘(不间断)| [kernel_thread] | wait_on_page_bit_common 1698 | 23.92 | (io_wqe_worker- *)|正在运行(在CPU上)| [运行中] 0 54 | 0.76 | (fio)|正在运行(在CPU上)| [运行中] 0 18 | 0.25 | (kswapd *)|正在运行(在CPU上)| [运行中] 0 8 | 0.11 | (io_wqe_worker- *)|正在运行(在CPU上)| [kernel_thread] | 0 5 | 0.07 | (io_wqe_worker- *)|磁盘(不间断)| [运行中] 0 5 | 0.07 | (io_wqe_worker- *)|磁盘(不间断)| [运行中] wait_on_page_bit_common 5 | 0.07 | (kworker / *:*-事件)|正在运行(在CPU上)| [运行中] 0 4 | 0.06 | (fio)|正在运行(在CPU上)| io_uring_enter | io_cqring_wait 4 | 0.06 | (io_wqe_worker- *)|正在运行(在CPU上)| [kernel_thread] | wait_on_page_bit_common 1 | 0.01 | (fio)|正在运行(在CPU上)| io_uring_enter | 0 1 | 0.01 | (io_wqe_worker- *)|正在运行(在CPU上)| [kernel_thread] | io_wqe_worker 1 | 0.01 | (rcu_sched)|正在运行(在CPU上)| [运行中] 0

因此,不是我的3个fio进程神奇地消耗了所有CPU时间,而是有很多io_wge_worker-N内核线程正在执行某些操作!它开始看起来像“不是我,而是您-Linux内核”。当向右滚动上述输出时,您会看到最上面的条目报告这些线程在线程状态D中等待-不间断(通常)磁盘I / O睡眠,而请求睡眠的内核函数(wchan)是wait_on_page_bit_common。这是每次您在缓存的I / O(页面缓存)代码路径中等待时都会显示的常见WHAN。我们仍然不知道这仅仅是“等待通过页面缓存完成缓慢的I / O”场景还是某些内核问题。请记住,有很多这样的内核工作线程也在内核模式下燃烧CPU,而不是等待任何东西(上面第二行突出显示的行带有“ Running(ON CPU)”)。

好消息是我们可以轻松地进一步深入分析! pSnapper还允许您对/ proc / PID / stack进行采样,以大致了解任何线程位于哪个内核位置。您必须一直向右滚动,直到看到突出显示的功能:

$ sudo psn -G syscall,wchan,kstack Linux进程快照程序v0.18,由Tanel Poder [https://0x.tools]采样/ proc / syscall,堆栈,wchan,stat持续5秒钟...已完成。===活动线程================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ================================================== ==================================================样品| avg_threads |通讯|州| syscall | wchan | kstack ------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- -------------------------------------------------- ------------------- 686 | 19.06 | (io_wqe_worker- *)|正在运行(在CPU上)| [运行中] 0 | -418 | 11.61 | (io_wqe_worker- *)|磁盘(不间断)| [kernel_thread] | wait_on_page_bit_common | kthread()-> io_wqe_worker()-> io_worker_handle_work()-> io_wq_submit_work()-> io_issue_sqe()-> io_read()-> blkdev_read_iter()-> generic_file_read_it_buffer )-> wait_on_page_bit_common()384 | 10.67 | (io_wqe_worker- *)|正在运行(在CPU上)| [运行中] 0 | kthread()-> io_wqe_worker()-> io_worker_handle_work()-> io_wq_submit_work()-> io_issue_sqe()-> io_read()-> blkdev_read_iter()-> generic_file_read_it_buffer )-> wait_on_page_bit_common()284 | 7.89 | (io_wqe_worker- *)|正在运行(在CPU上)| [运行中] 0 | kthread()-> io_wqe_worker()-> io_worker_handle_work()-> io_wq_submit_work()-> io_issue_sqe()-> io_read()-> blkdev_read_iter()-> generic_file_read_it_buffer )-> page_cache_sync_readahead()-> force_page_cache_readahead()-> page_cache_readahead_unbounded()-> __ page_cache_alloc()-> alloc_pages_current()-> __ alloc_pages_nodemask()-> __ alloc_pages_slowto(免费)。 > do_try_to_free_pages()-> shrink_zones()-> shrink_node()-> shrink_node_memcgs()-> shrink_lruvec()-> shrink 233 | 6.47 | (io_wqe_worker- *)|正在运行(在CPU上)| [运行中] 0 | kthread()-> io_wqe_worker()-> io_worker_handle_work()-> io_wq_submit_work()-> io_issue_sqe()-> io_read()-> blkdev_read_iter()-> generic_file_read_it_buffer )-> page_cache_sync_readahead()-> force_page_cache_readahead()-> page_cache_readahead_unbounded()-> __ page_cache_alloc_alloc()->

......