请教 Go 并发上传多个文件问题

2019-10-16 17:27:26 +08:00
 raywong

基于 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

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

5597 次点击
所在节点    Go 编程语言
29 条回复
raywong
2019-10-17 11:35:03 +08:00
@reus 好的,多谢
raywong
2019-10-17 11:43:17 +08:00
@zhshch 所以说时间大部分都是消耗在传输上了,如果忽略传输时间,那么开多个线程对磁盘写入会提升速度吗?
encro
2019-10-17 12:42:16 +08:00
你这个代码,本身就是忽略了传输时间的,调用你这个函数的时候,文件已经传输完成了,
所以你测试得到的时间就基本是写入时间,
开多个协程基本不会对提升速度(排除写入缓冲的情况),
因为硬盘的物理速度是核定的。
encro
2019-10-17 12:44:38 +08:00
要想提升速度,除非将文件存储到不同的物理 storage(比如挂在多个磁盘,阿里云同时存多个 oss bucket)
raywong
2019-10-17 13:29:36 +08:00
@encro 明白了,就像 7 楼说的写入到不同的磁盘会提升速度那样。所以这里的瓶颈就是在于磁盘,不是开多几个线程就能解决的,多谢~
Reficul
2019-10-17 15:31:00 +08:00
@raywong HTTP 报文发来的时候所有内容都在 Body 里,发来的时候就是一个流,文件被编码在里面。

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

要是石头磁盘,开多个线程往不通磁盘写可能会快点吧。。。
flyingghost
2019-10-17 15:58:36 +08:00
你这测试。。。根本没测到点子上吧?业务逻辑设计思路也有问题。

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

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

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

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

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

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

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

另外吐槽一点,MultipartForm 上传多文件已经是古代技术了。应用层处理需要的请求缓存和内存占用都会大一些。偶尔场景少量小文件传输无所谓,大量的,体积大的文件,我更倾向于 rest 风格的单文件直接 PUT。
raywong
2019-10-18 10:03:37 +08:00
@flyingghost 感谢老哥回复那么多。

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

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

使用 MultipartForm 的原因是文件也不大,还要求可以多文件上传(貌似写入缓冲会提升速度?)。
raywong
2019-10-18 10:40:55 +08:00
@Reficul 嗯 谢谢,在这之前没想到网络传输的问题,单纯地想并发写入磁盘就会变快(太年轻了...)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/609995

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX