基于 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
请问各位大佬这是什么原因...
1
forcecharlie 2019-10-16 17:33:33 +08:00
据我所知,带宽和 I/O 是有限的。
|
2
TypeErrorNone 2019-10-16 17:55:37 +08:00
消耗时间在上传的网络和磁盘的 io 上,这并不是开启几个协程解决的,你在代码开多个协程处理文件,是已经上传到服务器的资源。
|
3
lbp0200 2019-10-16 18:19:34 +08:00
改成传 2 个,看看是不是带宽导致的
|
5
zhshch 2019-10-16 19:23:32 +08:00
在下载任务里,很多时间耽误在传输和等待,所以开并发有提升。
但是在上传任务里,带宽已然被挤满了(瓶颈在客户端或者服务器),开多个线程也不会改变网络固有的传输能力。 |
6
raywong OP |
7
tiedan 2019-10-16 19:24:11 +08:00
你多加几块磁盘,然后并发在不同磁盘同时写,你就会发现比同步写快了
|
8
reus 2019-10-16 23:19:07 +08:00 via Android
这样做没有意义,因为一个请求是必然顺序传输到服务器,你开多少线程,都不会影响传输速度。
反而可能造成问题,如果有人构造一个几百个文件的请求,你也开几百个 goroutine 吗… 这里按顺序处理就行。 |
9
encro 2019-10-17 09:19:58 +08:00
我的理解是:
正确的测试方法是开启多个 client 同时上传到 server, 单个 server function 里面没有必要再 goroutine,因为 gin 本身执行 server function 就是采用了 goroutine 吧。这里面的瓶颈很可能在于 disk I/O。 |
10
Reficul 2019-10-17 09:22:50 +08:00 via Android
看一下 HTTP 报文,你就知道一个请求里,文件都是一个个排队发过来的。
|
14
encro 2019-10-17 10:03:41 +08:00
磁盘速度只有这么快,你用多少个线程,已经不重要了。
“一个妈妈怀胎 10 个月,10 个妈妈还是 10 个月”。 |
15
encro 2019-10-17 10:04:58 +08:00
如果再 unix/linux 上,试试直接复制到 /dev/null,应该快很多。
|
17
reus 2019-10-17 10:16:09 +08:00
@raywong 是打包成一个 multipart form 发送,不是排队发送。进入这个处理函数的时候,应该是已经 parse 完了的,所以你开不开 goroutine,前面的都没影响。开不开 goroutine,区别是并行写入磁盘与否,这里没有区别,就说明磁盘 io 不是瓶颈。
用快递比喻,就是几个订单(文件)都用一个包裹(请求)发送给你,你有多少个人拆,都影响不了物流过程。而拆箱过程很短,你一个人拆和几个人拆时间都差不多。 |
18
raywong OP @reus 所以意思是说时间大部分都是消耗在传输上,文件越多传输得也自然就越慢了。还有一个问题就是这里确实是并发将文件写入磁盘了吗(忽略传输时间)?换句话说开不开 goroutine 对磁盘写入有没有影响。
|
20
lazyfighter 2019-10-17 11:33:29 +08:00
那是不是说明,瓶颈不再磁盘上,而是在上传本身上
|
23
encro 2019-10-17 12:42:16 +08:00
你这个代码,本身就是忽略了传输时间的,调用你这个函数的时候,文件已经传输完成了,
所以你测试得到的时间就基本是写入时间, 开多个协程基本不会对提升速度(排除写入缓冲的情况), 因为硬盘的物理速度是核定的。 |
24
encro 2019-10-17 12:44:38 +08:00
要想提升速度,除非将文件存储到不同的物理 storage(比如挂在多个磁盘,阿里云同时存多个 oss bucket)
|
25
raywong OP @encro 明白了,就像 7 楼说的写入到不同的磁盘会提升速度那样。所以这里的瓶颈就是在于磁盘,不是开多几个线程就能解决的,多谢~
|
26
Reficul 2019-10-17 15:31:00 +08:00
@raywong HTTP 报文发来的时候所有内容都在 Body 里,发来的时候就是一个流,文件被编码在里面。
并行地读请求数据并写入磁盘不会变快是因为网络 IO 的带宽肯定比磁盘小。。。。 要是石头磁盘,开多个线程往不通磁盘写可能会快点吧。。。 |
27
flyingghost 2019-10-17 15:58:36 +08:00
你这测试。。。根本没测到点子上吧?业务逻辑设计思路也有问题。
先来梳理一下上传文件有哪些瓶颈。 客户端磁盘读 - 浏览器单站点连接数 - 客户端网络速度 - 服务端网络速度 - 服务端应用处理速度 - 服务端磁盘写 对于单客户端来说,磁盘读一般不会造成瓶颈,更多的瓶颈是网络传输上。 对于服务端来说,网速很重要,但磁盘写入也重要了,因为它要并行处理多个客户端。 所以单机测试的话,最大瓶颈容易出在网速。这时候分多个协程是没有任何帮助的。 多机测的时候,客户端网速一般可以不考虑了,带宽窄但人多啊。这时候瓶颈容易出现在服务端网速 和 服务端磁盘。 真实压测,当然要用多 client 一起测,尤其对于上传文件这种场景来说。 但是多机天然就多连接,服务端伺服多个连接天然就并发了。有没有必要把 n*client 个上传过程再拆一步,n 个 client 每 client m 个文件变成 n*m 个连接 /goroutine 呢?这才是业务逻辑需要考虑的。 因为这直接影响到一个重要因素:传输失败率。 很多真实场景下,和服务端维持长期稳定传输是一件容易失败的事情。多个大文件捏一起,总时间更长,失败几率更高,无效传输时长更多,整体来看有效上传速度是降低了。文件越大越明显。 常见的办法就是多文件分开多个链接传输。甚至对于巨大文件,客户端直接分片给服务端。 优点是失败率降低吞吐率提高。缺点是上传逻辑更复杂,占用服务端连接数更多。 以上都是单点 server 的情况。多点的话又是另一种思路。 另外吐槽一点,MultipartForm 上传多文件已经是古代技术了。应用层处理需要的请求缓存和内存占用都会大一些。偶尔场景少量小文件传输无所谓,大量的,体积大的文件,我更倾向于 rest 风格的单文件直接 PUT。 |
28
raywong OP @flyingghost 感谢老哥回复那么多。
一开始确实没考虑那么多,只是想开多个协程看看能不能将多个文件并发写入磁盘,从而加快速度(单 client ),没想到多个 client 上传的场景(项目都是一些小服务,并发很小,所以没考虑到多个 client 的情况) 要是有 n 个 client 上传 m 个文件的话,传输失败率的确得考虑,好像听说有遇到这样的事(不是我负责的服务),要是采用多文件分开多个链接传输的方式的话,客户应该不怎么会接受吧?毕竟还是想要“方便”... 使用 MultipartForm 的原因是文件也不大,还要求可以多文件上传(貌似写入缓冲会提升速度?)。 |