请教 Go 并发上传多个文件问题 - V2EX
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 6126 次点击
    这是一个创建于 2191 天前的主题,其中的信息可能已经有所发展或是发生改变。

    基于 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     Solana     2703 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 42ms UTC 15:04 PVG 23:04 LAX 08:04 JFK 11:04
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86