英文:
Paradoxical performance serving samba files in go
问题
我已经用Go编写了一个程序,它作为一个简单的HTTP接口连接到一个Samba共享:用户通过GET请求访问http://proxy.example.com/?path=\\repository\foo\bar.txt
,然后通过http.ServeFile
来提供\\repository\foo\bar.txt
文件。
然而,性能非常糟糕。我进行了一些基准测试,结果让我困惑不已。为了背景,这里有三台机器:Samba服务器(文件所在的地方)、代理服务器(运行Go程序的地方)和最终要获取文件的终端用户机器。Samba服务器和代理服务器位于同一地点,而终端用户则相对较远。
使用Windows的复制功能,从Samba机器直接复制到用户机器的速度约为1.5MB/s。这对我来说已经足够好了,也是我在代理服务中的目标。
不幸的是,从用户机器上运行curl 'http://proxy.example.com/?path=\\repository\foo\bar.txt' > bar.txt
的速度只有约150KB/s。
那么,让我们看看Samba服务器和代理服务器之间是否存在连接问题。从Samba服务器复制到代理服务器的速度大约为15MB/s。
嗯,也许是Go的问题?我将编写一个Go程序来测试传输速度。
src, _ := os.Open("\\\\repository\\foo\\bar.txt")
start := time.Now()
written, _ := io.Copy(ioutil.Discard, src)
elapsed := time.Since(start)
bytesPerSecond := written/int64(elapsed/time.Second)
天啊,速度是15MB/s。
好吧,好吧,也许是Go代码中的其他问题导致了这个问题。远程登录到代理服务器,启动IE,访问http://proxy.example.com/?path=\\repository\foo\bar.txt
,速度还是15MB/s。
好吧,显然我的代码运行得很好,肯定是代理服务器和终端用户之间的连接有问题。我将bar.txt
复制到代理服务器,并在URL中使用它的本地路径\mycoolfiles\bar.txt
。嗯,速度只有1.5MB/s。
更奇怪的是,我刚好将C:\mycoolfiles
设置为名为\\alexscoolfiles
的网络共享,而http://proxy.example.com/?path=\\alexscoolfiles\bar.txt
的速度是,咚咚咚,150KB/s。
为了确认这个问题,我将Go程序更改为两个步骤运行:
- 从共享目录复制文件到本地硬盘
- 从本地硬盘使用
http.SendFile
发送文件
瞧,经过短暂的暂停,文件以15MB/s的速度传输完毕后,下载速度稳定在1.5MB/s。
所以,共享->代理的速度是15MB/s,代理->用户的速度是1.5MB/s,但是共享->代理->用户的速度是... 150KB/s?比应该的速度慢了十倍?除非你在代理服务器上,因为那样的话速度就正好是应该的?而且,只要一个是UNC路径,另一个是本地路径,即使访问的是完全相同的文件,这个问题仍然存在?
这到底是怎么回事?
请帮帮我,我真的不知道该怎么办。
编辑:所以我的猜想是(正如评论中所提到的),这可能与TCP有关。问题已经被隔离到几乎只剩下io.Copy
。
- 我知道当读取器是Samba文件,写入器是
ioutil.Discard
时,我可以达到最大吞吐量。 - 我知道当读取器是本地文件,写入器是
http.Response
时,无论客户端消费响应的带宽和往返时间如何,我都可以达到最大吞吐量。 - 我知道当读取器是Samba文件,写入器是
http.Response
,连接是本地时,我可以达到最大吞吐量。 - 我知道当读取器是Samba文件,写入器是
http.Response
,连接不是本地时,吞吐量很差(约为原来的1/10)。
查看io.Copy
的代码,似乎唯一可能引起问题的是读取Samba文件和写入响应之间的时间配合;一个足够快的写入器使得读取Samba文件达到最大吞吐量,一个足够快的读取器使得http.Response.Write
达到最大吞吐量,但是将它们结合起来就会导致一切变慢。
非常有帮助的是... 到底发生了什么,更重要的是,我该如何解决这个问题。
英文:
I've written a program in go that acts as a simple HTTP interface to a samba share: the user makes a get request to http://proxy.example.com/?path=\\repository\foo\bar.txt
and \\repository\foo\bar.txt
is served up by way of http.ServeFile (more or less).
However, the performance is abysmal. I ran some benchmarks and the results have me stumped. For context, there are three machines in the picture: the samba server (where the file is located), the proxy server (where the go program runs) and the end user's machine (where I ultimately want the file to get). The samba server and the proxy server are co-located, and the end users are fairly far away.
A straight copy from the samba machine to the user machine using windows' copy runs at ~1.5MB/s. That's good enough for me, and what I'm aiming for in the proxy service.
Unfortunately curl 'http://proxy.example.com/?path=\\repository\foo\bar.txt' > bar.txt
from a user's machine clocks in at about 150KB/s.
So, let's see if there's connectivity issues between the samba server and the proxy server. A copy from the samba server to the proxy server looks like it's getting... about 15MB/s.
Hm, maybe it's a go thing? I'll write a go program that benchmarks the transfer speed.
src, _ := os.Open("\\\\repository\\foo\\bar.txt")
start := time.Now()
written, _ := io.Copy(ioutil.Discard, src)
elapsed := time.Since(start)
bytesPerSecond := written/int64(elapsed/time.Second)
Dang, 15MB/s.
Ok, ok, maybe there's something else in the go code causing the issue. Remote onto the proxy server, start up IE, go to http://proxy.example.com/?path=\\repository\foo\bar.txt
, 15MB/s.
Alright, so my code is obviously working great, it must be the connection between the proxy server and the end user. I'll copy bar.txt
to the proxy server and use its local path in the url, \mycoolfiles\bar.txt
. Huh, 1.5MB/s.
To make things even weirder, I just happen to have C:\mycoolfiles
set up as a network share named \\alexscoolfiles
, and http://proxy.example.com/?path=\\alexscoolfiles\bar.txt
clocks in at, dun dun dun, 150KB/s.
Just to confirm this madness, I changed the go program to run in two steps:
- Copy the file from the share to the local hard drive
- http.SendFile from there
Lo and behold, after a short pause while the file transfers over at 15MB/s, the download begins at a solid 1.5MB/s.
So, share->proxy is 15MB/s, and proxy->user is 1.5MB/s, but share->proxy->user is... 150KB/s? Ten times slower than it should be? Except not when you're on the same machine as the proxy, because then it's exactly as fast as it should be? And further this problem exists even if it's the exact same file being accessed as long as one is a UNC path and the other is just a local path?
WHAT?
Please help, I just have no idea.
EDIT: So my hunch is (as was commented) that it has something to do with TCP. The offending code has been isolated to pretty much just io.Copy.
- I know that when the reader is a samba file, and the writer is ioutil.Discard, I get about maximum throughput.
- I know that when the reader is a local file and the writer is an http.Response, I get about maximum throughput regardless of the bandwidth and RRT of the client consuming the response.
- I know that when the reader is a samba file, the writer is an http.Response, and the connection is local, I get about maximum throughput.
- I know that when the reader is a samba file, the writer is an http.Response, and the connection is not local, I get terrible (~1/10) throughput.
Looking through io.Copy, it seems the only thing that could cause the issue is in the interplay between the timings of reading the samba file and writing the response; a sufficiently fast writer makes reading a samba file reach max throughput, a sufficiently fast reader makes http.Response.Write reach max throughput, but combining them makes everything suck.
What would be very helpful is... What is actually happening and, more importantly, how can I make this problem go away.
答案1
得分: 1
在试图追踪问题出现的确切位置和情况后,我最终将问题归结为一行代码:如果我注释掉io.Copy
中使用ReadFrom
的部分(当前位于io/io.go
的第358行),那么我可以获得最大吞吐量。
查看ReadFrom
的实现位置(位于net/http/server.go
的第381行):
// ReadFrom is here to optimize copying from an *os.File regular file
// to a *net.TCPConn with sendfile.
我真的没有继续深入挖掘的意愿,但我猜测这会调用net/sendfile_windows.go
,它调用了TransmitFile系统调用,在我们的各种服务器配置和这之间发生了一些不好的事情。
解决方案是从http.ServeFile
中复制并粘贴所需的内容,然后在将其传递给io.Copy
之前将ResponseWriter
转换为io.Writer
(或其他类型):
type writerOnly struct {
io.Writer
}
//...
io.Copy(writerOnly{w}, f)
//...
英文:
After trying to track down exactly where and under what circumstances I could reproduce the issue, I finally got it down to one line: if I commented out where io.Copy uses ReadFrom (currently io/io.go
, line 358), I got maximum throughput.
Checking out where ReadFrom is implemented (net/http/server.go
, line 381):
// ReadFrom is here to optimize copying from an *os.File regular file
// to a *net.TCPConn with sendfile.
I don't really have the will to dig any deeper, but I'm guessing this calls net/sendfile_windows.go
, which calls the TransmitFile syscall, and somewhere between that and our various server configs something not good happens.
The solution was to copy and paste the things I needed from http.ServeFile, and then turn the ResponseWriter into an io.Writer (or whatever) before passing it to io.Copy:
type writerOnly struct {
io.Writer
}
//...
io.Copy(writerOnly{w}, f)
//...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论