随着我在网络中对Raspberry Pi和其他设备的实验,我创建了一个小型的网络应用,以帮助使用组播的设备发现、数据收集和其他功能。

这个应用程序的一个主要特点是每周能够从一些插件中下载各种数据和指标。由于应用一些压缩后的文件大小从200MB到250MB不等,因此必须仔细考虑使用Go通过TCP发送这些文件的一些方法。

在这篇文章中,我们将探讨在linux中使用Go通过TCP发送大文件的一些方法和技巧,同时考虑到小设备的限制以及高效可靠的文件传输的重要性。

Naive的方法

尽管这段代码看起来很简单,但它在效率方面有一个很大的缺点。该代码在一个循环中把数据从源的内核缓冲区移动到用户空间的缓冲区,然后立即从该缓冲区复制到目的地的内核缓冲区。这种数据的双重复制导致了性能的损失,因为缓冲区只是作为一个临时存放的地方。

buf

此外,数据的双重复制也增加了内存的使用,因为源和目的缓冲区都必须在内存中分配和维护。这可能会使系统的资源紧张,特别是在传输大文件和设备较小的情况下。

下图对通过TCP发送文件时的数据流进行了简化说明。使用前面的方法,需要注意的是,在这个过程完成之前,数据被复制了四次:

diskread bufferread bufferapp bufferapp buffersocket buffersocket buffer

这突出了多次复制数据的低效率,这还没有提到用户模式和内核模式之间的多次上下文切换。

read()
write/send()write/send()

什么是DMA?

DMA是直接内存访问的意思。它是一种允许外围设备直接访问计算机内存的技术,不需要CPU,以加快数据传输速度。通过这种方式,CPU从执行数据传输本身中解放出来,使其能够执行其他任务,使系统更有效率。 直接内存访问

为了优化文件传输过程,我们必须尽量减少缓冲区拷贝和上下文切换的数量,并减少将数据从一个地方移动到另一个地方的开销。

使用一个专门的系统调用"sendfile"

syscall

sendfile()在一个文件描述符和另一个文件描述符之间复制数据。因为这种复制是在内核中完成的,所以sendfile()比read(2)和write(2)的组合更有效,后者需要在用户空间传输数据。
https://man7.org/linux/man-pages/man2/sendfile.2.html

sendfile
sendfile

当然,这种"zero-copy"是从用户模式的应用角度来看的。

这种情况下有两个DMA拷贝+一个CPU拷贝,以及两个上下文切换。

sendfile

集合是指网络接口卡(NIC)从多个内存位置接收数据,并在通过网络传输之前将其合并到一个数据缓冲区的能力。网卡的分散/聚集功能用于提高数据传输的效率,减少传输数据所需的内存拷贝数量。NIC可以将数据复制到一个缓冲区,而不是将多个缓冲区的数据收集到一个缓冲区,从而减少CPU负载,提高传输的性能。

Nic与gather支持

这种情况下,只有两个DMA拷贝和两个上下文切换。

因此,减少缓冲区副本的数量不仅可以提高性能,还可以减少内存的使用,使文件传输过程更加高效和可扩展。

请注意,所提供的插图和情景是高度简化的,并不完全代表这些过程的复杂性。然而,我们的目的是以一种直截了当和易于理解的方式来介绍这些信息。

为什么Go中经常推荐"io.Copy"?

io.Copy

https://www.sangniao.com/

在Go中使用io.Copy的好处不仅仅是它的32k缓冲区管理和优化src:

ReadFromReadFromdstTCPConnio.Copy
io.copysendfile
stracesendfile
ReadFromio.Copysendfilesplice
WriteToio.Copyio.Copy

对Linux可能的提示

我还试图通过增加网络接口的MTU(最大传输单元)大小和改变TCP缓冲区大小来提高Linux系统在一般情况下的性能。

tcp_wmemtcp_rmem
tcp_wmem
tcp_rmem

增加这两个值会要求更多的内存使用。

对我来说,由于某些限制,如一些设备、本地网络等的限制,这些优化没能带来实质性的改善。

总而言之

sendfileio.Copysendfile

谢谢你花时间阅读这篇文章。我希望它提供了一些有用的信息。我不断努力提高我的理解和知识,所以我感谢你的反馈或更正。再次感谢您的时间和考虑。


本文标题:用Golang在Linux中优化大文件传输:TCP和Syscall的探索