我进行了一些基准测试以提出一些通用准则。我用大约 500k 的小(~14kb)文件进行了测试。我认为中型文件的结果应该相似;但对于较大的文件,我怀疑磁盘争用变得更加严重。如果对操作系统/硬件内部有更深入了解的人可以用更具体的解释来补充这个答案,解释为什么有些事情比其他事情快,我们将不胜感激。
我使用具有双通道 RAM 和 Linux 内核 4.18 的 16 个虚拟内核(8 个物理)计算机进行了测试。
多线程会增加读取吞吐量吗?
答案是肯定的。我认为这可能是由于 1)单线程应用程序的硬件带宽限制或 2)当许多线程发出请求时,操作系统的磁盘请求队列得到了更好的利用。最好的性能是使用virtual_cores*2 线程。除此之外,吞吐量会缓慢下降,这可能是因为磁盘争用增加。如果页面碰巧缓存在 RAM 中,那么最好有一个大小为virtual_cores 的线程池。但是,如果 virtual_cores*2 就可以了。
我认为virtual_cores*2 比virtual_cores 更好的原因是文件读取还包括一些与磁盘无关的延迟,如系统调用、解码等。所以也许处理器可以更有效地交错线程:当一个在磁盘上等待时,第二个可以执行与磁盘无关的文件读取操作。 (会不会也是因为内存是双通道的?)
我测试了读取随机文件与顺序读取(通过查找文件在存储中的物理块位置,并以此排序请求)。顺序访问对 HDD 带来了相当大的改进,这是意料之中的。如果您的应用程序中的限制因素是文件读取时间,而不是处理所述文件,我建议您重新排序顺序访问请求以获得提升。
有可能使用异步磁盘 IO,而不是线程池。但是,根据我的阅读,似乎还没有一种可移植的方式来做到这一点(see this reddit thread)。此外,libuv 支持 NodeJS uses a thread pool 处理其文件 IO。
平衡读取与处理吞吐量
理想情况下,我们可以在单独的线程中进行读取和处理。当我们处理第一个文件时,我们可以在另一个线程中排队下一个文件。但是我们为读取文件分配的线程越多,与处理线程的 CPU 争用就越多。解决方案是提供更快的操作(读取与处理)最少的线程数,同时仍然在文件之间提供零处理延迟。这个公式在我的测试中似乎给出了很好的结果:
prop = read_time/process_time
if prop > 1:
# double virtual core count gives fastest reads, as per tests above
read_threads = virtual_cores*2
process_threads = ceil(read_threads/(2*prop))
else:
process_threads = virtual_cores
# double read thread pool so CPU can interleave better, as mentioned above
read_threads = 2*ceil(process_threads*prop)
例如: Read = 2s, Process = 10s;所以每 5 个处理线程就有 2 个读取线程
在我的测试中,额外的读取线程只会导致大约 1-1.5% 的性能损失。在我的测试中,对于接近于零的prop,1 个读取 + 16 个进程线程的吞吐量与 32 个读取 + 16 个进程线程的吞吐量几乎相同。现代线程应该是相当轻量级的,如果文件没有被足够快地消耗,读取线程无论如何都应该处于休眠状态。 (当prop 很大时,进程线程也应该如此)
另一方面,阅读线程太少会产生更大的影响(我的第三个原始问题)。例如,对于一个非常大的prop,1 个读取 + 16 个进程线程比 1 个读取 + 15 个进程线程慢 36%。由于进程线程占用了基准计算机的所有内核,读取线程有太多的 CPU 争用,并且有 36% 的时间无法排队等待下一个要处理的文件。所以,我的建议是错误地支持过多的读取线程。按照我上面的公式将读取线程池大小加倍应该可以做到这一点。
旁注:您可以通过将virtual_cores 设置为可用内核的较小百分比来限制应用程序消耗的 CPU 资源。您也可以选择放弃加倍,因为当有一个或更多的空闲内核未执行更密集的处理线程时,CPU 争用可能不是问题。
总结
根据我的测试结果,使用具有virtual_cores*2 文件读取线程+virtual_cores 文件处理线程的线程池,将在各种不同的时序场景下为您提供良好的性能。此配置应为您提供最大吞吐量的约 2%,而无需花费大量时间进行基准测试。