请教 Golang Go语言中并发上传多个文件问题

发布于 1周前 作者 sinazl 来自 Go语言

请教 Golang Go语言中并发上传多个文件问题

基于 Gin 框架,在前端上传多文件到后台时(写入磁盘)使用了 goroutines,奇怪的是虽然并发执行了,但是上传消耗的时间却跟同步上传(没有使用 goroutines )差不多,难道是我使用的姿势不对?还是说多文件上传不能使用协程?

代码:

func UploadFileHandler(ctx *gin.Context) {
	formData, _ := ctx.MultipartForm()
    files := formData.File["fileList"]
start := time.Now()
var wg sync.WaitGroup
wg.Add(len(files))

for _, file := range files {
	go func(file *multipart.FileHeader) {
		
		fmt.Printf("(%s) upload...\n", file.Filename)
		
		// 文件上传
		filePath := filepath.Join(dirPath, file.Filename)
		errors = ctx.SaveUploadedFile(file, filePath)
		if errors != nil {
			ctx.JSON( http.StatusBadRequest, gin.H{
				"code": 400,
		        "error" : errors.Error(),
		    })
		}
		
		fmt.Printf("(%s) upload end...\n", file.Filename)
        wg.Done()

	}(file)
}

wg.Wait()

end := time.Since(start)
fmt.Printf("it takes %s\n", end)

ctx.JSON( http.StatusOK, gin.H{
	"code": 200,
	"msg": "上传成功",
})

}

执行结果:

(文件 4.zip) upload...
(文件 2.zip) upload...
(文件 3.zip) upload...
(文件 1.zip) upload...
(文件 4.zip) upload end...
(文件 2.zip) upload end...
(文件 1.zip) upload end...
(文件 3.zip) upload end...
it takes 713.0408ms

下面是没有使用协程的方式的执行结果:

(文件 4.zip) upload...
(文件 4.zip) upload end...
(文件 3.zip) upload...
(文件 3.zip) upload end...
(文件 2.zip) upload...
(文件 2.zip) upload end...
(文件 1.zip) upload...
(文件 1.zip) upload end...
it takes 730.0474ms

请问各位大佬这是什么原因...


更多关于请教 Golang Go语言中并发上传多个文件问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

30 回复

据我所知,带宽和 I/O 是有限的。

更多关于请教 Golang Go语言中并发上传多个文件问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


消耗时间在上传的网络和磁盘的 io 上,这并不是开启几个协程解决的,你在代码开多个协程处理文件,是已经上传到服务器的资源。

改成传 2 个,看看是不是带宽导致的

在本地测试的,一样结果

在下载任务里,很多时间耽误在传输和等待,所以开并发有提升。

但是在上传任务里,带宽已然被挤满了(瓶颈在客户端或者服务器),开多个线程也不会改变网络固有的传输能力。

你多加几块磁盘,然后并发在不同磁盘同时写,你就会发现比同步写快了

这样做没有意义,因为一个请求是必然顺序传输到服务器,你开多少线程,都不会影响传输速度。
反而可能造成问题,如果有人构造一个几百个文件的请求,你也开几百个 goroutine 吗…
这里按顺序处理就行。

我的理解是:
正确的测试方法是开启多个 client 同时上传到 server,
单个 server function 里面没有必要再 goroutine,因为 gin 本身执行 server function 就是采用了 goroutine 吧。这里面的瓶颈很可能在于 disk I/O。

看一下 HTTP 报文,你就知道一个请求里,文件都是一个个排队发过来的。

是一个个排队发送的原因吗,后台不是拿到全部文件后再写入磁盘的?

这里 disk I/O 的影响,也就是说还是要一个个排队写入 disk 么?

可能会限制一下一次性能上传多少个文件。虽然是顺序传输到服务器,服务器不是拿到全部文件后再写入磁盘的?

磁盘速度只有这么快,你用多少个线程,已经不重要了。

“一个妈妈怀胎 10 个月,10 个妈妈还是 10 个月”。

如果再 unix/linux 上,试试直接复制到 /dev/null,应该快很多。

明白了 谢谢。

是打包成一个 multipart form 发送,不是排队发送。进入这个处理函数的时候,应该是已经 parse 完了的,所以你开不开 goroutine,前面的都没影响。开不开 goroutine,区别是并行写入磁盘与否,这里没有区别,就说明磁盘 io 不是瓶颈。

用快递比喻,就是几个订单(文件)都用一个包裹(请求)发送给你,你有多少个人拆,都影响不了物流过程。而拆箱过程很短,你一个人拆和几个人拆时间都差不多。

所以意思是说时间大部分都是消耗在传输上,文件越多传输得也自然就越慢了。还有一个问题就是这里确实是并发将文件写入磁盘了吗(忽略传输时间)?换句话说开不开 goroutine 对磁盘写入有没有影响。

看看这一段代码运行了多久就知道了

那是不是说明,瓶颈不再磁盘上,而是在上传本身上

好的,多谢

所以说时间大部分都是消耗在传输上了,如果忽略传输时间,那么开多个线程对磁盘写入会提升速度吗?

你这个代码,本身就是忽略了传输时间的,调用你这个函数的时候,文件已经传输完成了,
所以你测试得到的时间就基本是写入时间,
开多个协程基本不会对提升速度(排除写入缓冲的情况),
因为硬盘的物理速度是核定的。

要想提升速度,除非将文件存储到不同的物理 storage(比如挂在多个磁盘,阿里云同时存多个 oss bucket)

明白了,就像 7 楼说的写入到不同的磁盘会提升速度那样。所以这里的瓶颈就是在于磁盘,不是开多几个线程就能解决的,多谢~

HTTP 报文发来的时候所有内容都在 Body 里,发来的时候就是一个流,文件被编码在里面。

并行地读请求数据并写入磁盘不会变快是因为网络 IO 的带宽肯定比磁盘小。。。。

要是石头磁盘,开多个线程往不通磁盘写可能会快点吧。。。

你这测试。。。根本没测到点子上吧?业务逻辑设计思路也有问题。

先来梳理一下上传文件有哪些瓶颈。

客户端磁盘读 - 浏览器单站点连接数 - 客户端网络速度 - 服务端网络速度 - 服务端应用处理速度 - 服务端磁盘写
对于单客户端来说,磁盘读一般不会造成瓶颈,更多的瓶颈是网络传输上。
对于服务端来说,网速很重要,但磁盘写入也重要了,因为它要并行处理多个客户端。

所以单机测试的话,最大瓶颈容易出在网速。这时候分多个协程是没有任何帮助的。
多机测的时候,客户端网速一般可以不考虑了,带宽窄但人多啊。这时候瓶颈容易出现在服务端网速 和 服务端磁盘。

真实压测,当然要用多 client 一起测,尤其对于上传文件这种场景来说。

但是多机天然就多连接,服务端伺服多个连接天然就并发了。有没有必要把 nclient 个上传过程再拆一步,n 个 client 每 client m 个文件变成 nm 个连接 /goroutine 呢?这才是业务逻辑需要考虑的。

因为这直接影响到一个重要因素:传输失败率。
很多真实场景下,和服务端维持长期稳定传输是一件容易失败的事情。多个大文件捏一起,总时间更长,失败几率更高,无效传输时长更多,整体来看有效上传速度是降低了。文件越大越明显。
常见的办法就是多文件分开多个链接传输。甚至对于巨大文件,客户端直接分片给服务端。
优点是失败率降低吞吐率提高。缺点是上传逻辑更复杂,占用服务端连接数更多。

以上都是单点 server 的情况。多点的话又是另一种思路。

另外吐槽一点,MultipartForm 上传多文件已经是古代技术了。应用层处理需要的请求缓存和内存占用都会大一些。偶尔场景少量小文件传输无所谓,大量的,体积大的文件,我更倾向于 rest 风格的单文件直接 PUT。

感谢老哥回复那么多。

一开始确实没考虑那么多,只是想开多个协程看看能不能将多个文件并发写入磁盘,从而加快速度(单 client ),没想到多个 client 上传的场景(项目都是一些小服务,并发很小,所以没考虑到多个 client 的情况)

要是有 n 个 client 上传 m 个文件的话,传输失败率的确得考虑,好像听说有遇到这样的事(不是我负责的服务),要是采用多文件分开多个链接传输的方式的话,客户应该不怎么会接受吧?毕竟还是想要“方便”…

使用 MultipartForm 的原因是文件也不大,还要求可以多文件上传(貌似写入缓冲会提升速度?)。

嗯 谢谢,在这之前没想到网络传输的问题,单纯地想并发写入磁盘就会变快(太年轻了…)

在Go语言中实现并发上传多个文件,你可以利用Go的goroutine和channel来处理并发任务。以下是一个基本的思路:

  1. 文件读取:首先,你需要获取要上传的文件列表,可以使用os.Openioutil.ReadFile(在Go 1.16之后推荐使用osio包中的函数)来读取文件内容。

  2. 创建goroutine:为每个文件创建一个goroutine来执行上传操作。你可以使用HTTP客户端(如net/http包中的http.Client)来发送POST请求,将文件内容作为请求体上传。

  3. 管理并发:使用带缓冲的channel来控制并发数量,避免创建过多的goroutine导致系统资源耗尽。你可以根据实际需求调整channel的缓冲区大小。

  4. 等待完成:使用sync.WaitGroup来等待所有goroutine完成。在启动每个goroutine时,调用WaitGroup.Add(1),在goroutine结束时调用WaitGroup.Done(),并在主goroutine中调用WaitGroup.Wait()来阻塞,直到所有任务完成。

  5. 错误处理:在goroutine中处理上传过程中可能出现的错误,并将错误信息传递回主goroutine进行记录或处理。

通过上述步骤,你可以有效地实现并发上传多个文件的功能。注意,在实际应用中,还需要考虑网络超时、重试机制、错误日志记录等细节问题,以确保系统的健壮性和可靠性。

回到顶部