V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
raywong
V2EX  ›  Go 编程语言

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

  •  
  •   raywong · 2019-10-16 17:27:26 +08:00 · 5733 次点击
    这是一个创建于 1844 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    29 条回复    2019-10-18 10:40:55 +08:00
    forcecharlie
        1
    forcecharlie  
       2019-10-16 17:33:33 +08:00
    据我所知,带宽和 I/O 是有限的。
    TypeErrorNone
        2
    TypeErrorNone  
       2019-10-16 17:55:37 +08:00
    消耗时间在上传的网络和磁盘的 io 上,这并不是开启几个协程解决的,你在代码开多个协程处理文件,是已经上传到服务器的资源。
    lbp0200
        3
    lbp0200  
       2019-10-16 18:19:34 +08:00
    改成传 2 个,看看是不是带宽导致的
    raywong
        4
    raywong  
    OP
       2019-10-16 19:20:42 +08:00 via Android
    @lbp0200 在本地测试的,一样结果
    zhshch
        5
    zhshch  
       2019-10-16 19:23:32 +08:00
    在下载任务里,很多时间耽误在传输和等待,所以开并发有提升。

    但是在上传任务里,带宽已然被挤满了(瓶颈在客户端或者服务器),开多个线程也不会改变网络固有的传输能力。
    raywong
        6
    raywong  
    OP
       2019-10-16 19:24:01 +08:00 via Android
    tiedan
        7
    tiedan  
       2019-10-16 19:24:11 +08:00
    你多加几块磁盘,然后并发在不同磁盘同时写,你就会发现比同步写快了
    reus
        8
    reus  
       2019-10-16 23:19:07 +08:00 via Android
    这样做没有意义,因为一个请求是必然顺序传输到服务器,你开多少线程,都不会影响传输速度。
    反而可能造成问题,如果有人构造一个几百个文件的请求,你也开几百个 goroutine 吗…
    这里按顺序处理就行。
    encro
        9
    encro  
       2019-10-17 09:19:58 +08:00
    我的理解是:
    正确的测试方法是开启多个 client 同时上传到 server,
    单个 server function 里面没有必要再 goroutine,因为 gin 本身执行 server function 就是采用了 goroutine 吧。这里面的瓶颈很可能在于 disk I/O。
    Reficul
        10
    Reficul  
       2019-10-17 09:22:50 +08:00 via Android
    看一下 HTTP 报文,你就知道一个请求里,文件都是一个个排队发过来的。
    raywong
        11
    raywong  
    OP
       2019-10-17 09:46:57 +08:00
    @Reficul 是一个个排队发送的原因吗,后台不是拿到全部文件后再写入磁盘的?
    raywong
        12
    raywong  
    OP
       2019-10-17 09:51:15 +08:00
    @encro 这里 disk I/O 的影响,也就是说还是要一个个排队写入 disk 么?
    raywong
        13
    raywong  
    OP
       2019-10-17 09:53:48 +08:00
    @reus 可能会限制一下一次性能上传多少个文件。虽然是顺序传输到服务器,服务器不是拿到全部文件后再写入磁盘的?
    encro
        14
    encro  
       2019-10-17 10:03:41 +08:00
    磁盘速度只有这么快,你用多少个线程,已经不重要了。

    “一个妈妈怀胎 10 个月,10 个妈妈还是 10 个月”。
    encro
        15
    encro  
       2019-10-17 10:04:58 +08:00
    如果再 unix/linux 上,试试直接复制到 /dev/null,应该快很多。
    raywong
        16
    raywong  
    OP
       2019-10-17 10:15:21 +08:00
    @encro 明白了 谢谢。
    reus
        17
    reus  
       2019-10-17 10:16:09 +08:00
    @raywong 是打包成一个 multipart form 发送,不是排队发送。进入这个处理函数的时候,应该是已经 parse 完了的,所以你开不开 goroutine,前面的都没影响。开不开 goroutine,区别是并行写入磁盘与否,这里没有区别,就说明磁盘 io 不是瓶颈。

    用快递比喻,就是几个订单(文件)都用一个包裹(请求)发送给你,你有多少个人拆,都影响不了物流过程。而拆箱过程很短,你一个人拆和几个人拆时间都差不多。
    raywong
        18
    raywong  
    OP
       2019-10-17 11:14:41 +08:00
    @reus 所以意思是说时间大部分都是消耗在传输上,文件越多传输得也自然就越慢了。还有一个问题就是这里确实是并发将文件写入磁盘了吗(忽略传输时间)?换句话说开不开 goroutine 对磁盘写入有没有影响。
    reus
        19
    reus  
       2019-10-17 11:31:51 +08:00
    @raywong 看看这一段代码运行了多久就知道了
    lazyfighter
        20
    lazyfighter  
       2019-10-17 11:33:29 +08:00
    那是不是说明,瓶颈不再磁盘上,而是在上传本身上
    raywong
        21
    raywong  
    OP
       2019-10-17 11:35:03 +08:00
    @reus 好的,多谢
    raywong
        22
    raywong  
    OP
       2019-10-17 11:43:17 +08:00
    @zhshch 所以说时间大部分都是消耗在传输上了,如果忽略传输时间,那么开多个线程对磁盘写入会提升速度吗?
    encro
        23
    encro  
       2019-10-17 12:42:16 +08:00
    你这个代码,本身就是忽略了传输时间的,调用你这个函数的时候,文件已经传输完成了,
    所以你测试得到的时间就基本是写入时间,
    开多个协程基本不会对提升速度(排除写入缓冲的情况),
    因为硬盘的物理速度是核定的。
    encro
        24
    encro  
       2019-10-17 12:44:38 +08:00
    要想提升速度,除非将文件存储到不同的物理 storage(比如挂在多个磁盘,阿里云同时存多个 oss bucket)
    raywong
        25
    raywong  
    OP
       2019-10-17 13:29:36 +08:00
    @encro 明白了,就像 7 楼说的写入到不同的磁盘会提升速度那样。所以这里的瓶颈就是在于磁盘,不是开多几个线程就能解决的,多谢~
    Reficul
        26
    Reficul  
       2019-10-17 15:31:00 +08:00
    @raywong HTTP 报文发来的时候所有内容都在 Body 里,发来的时候就是一个流,文件被编码在里面。

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

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

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

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

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

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

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

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

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

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

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

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

    使用 MultipartForm 的原因是文件也不大,还要求可以多文件上传(貌似写入缓冲会提升速度?)。
    raywong
        29
    raywong  
    OP
       2019-10-18 10:40:55 +08:00
    @Reficul 嗯 谢谢,在这之前没想到网络传输的问题,单纯地想并发写入磁盘就会变快(太年轻了...)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   909 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 21:38 · PVG 05:38 · LAX 14:38 · JFK 17:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.