Hi,
I'm not sure it's an issue however possibly you can recommend some kind of trick to get this resolved.
I stream a lot of files using fasthttp but the summary streaming speed is about 20% slower than nginx streaming.
It's clear fasthttp was developed with requests concurrency and high-load environments in mind. But is there a way to get the same performance for file streaming?
It's absolutely possible I do something wrong in my code.
Please have a look it. I'll be grateful for any kind of advice.
Thanks.
package main
import (
"runtime"
"strings"
"time"
"github.com/valyala/fasthttp"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
rw := fasthttp.PathRewriteFunc(func(ctx *fasthttp.RequestCtx) []byte {
urlPart := strings.Split(string(ctx.Path()), "/")
return []byte("/" + urlPart[2] + "/" + urlPart[3])
})
fs := fasthttp.FS{
Root: "/var/spool/cache",
AcceptByteRange: true,
PathRewrite: rw,
Compress: false,
CacheDuration: time.Duration(1) * time.Hour,
}
h := fs.NewRequestHandler()
requestHandler := func(ctx *fasthttp.RequestCtx) {
urlPart := strings.Split(string(ctx.Path()), "/")
fileName := urlPart[4]
if len(urlPart) == 5 && (ctx.IsGet() == true || ctx.IsHead() == true) {
ctx.Response.Header.Set("Content-disposition", "attachment; filename="+fileName)
h(ctx)
} else {
ctx.NotFound()
}
}
var s fasthttp.Server
s.Concurrency = 262144
s.MaxKeepaliveDuration = time.Duration(2) * time.Second
s.ReadBufferSize = 16384
s.WriteTimeout = time.Duration(15) * time.Second
s.Handler = requestHandler
errListen := s.ListenAndServe("0.0.0.0:80")
if errListen != nil {
panic(errListen)
}
}
Short story: fasthttp works faster than nginx on small files (up to 8Kb on my laptop). When serving big files, its' performance it about 80% comparing to nginx performance, because go's sendfile code path isn't optimized yet comparing to nginx.
Below are random remarks regarding the provided code:
The following line is not necessary, since Go automatically sets GOMAXPROCS to runtime.NumCPU() starting from go 1.5:
runtime.GOMAXPROCS(runtime.NumCPU())
The following line may panic, since it assumes that the requested path contains at least four parts delimited by '/' . This is not true for arbitrary user input:
return []byte("/" + urlPart[2] + "/" + urlPart[3])
This line also makes at least two memory allocations - the first one for string concatenation and the second one for string to []byte conversion. This may degrade performance a bit.
An additional memory allocation is made on the line for []string slice returned from strings.Split:
urlPart := strings.Split(string(ctx.Path()), "/")
I'd recommend rewriting rw in the following way:
rw := func(ctx *fasthttp.RequestCtx) []byte {
path := ctx.Path()
// fasthttp guarantees that the path starts with '/'
n := bytes.IndexByte(path[1:], '/')
if n < 0 {
return []byte("/invalid-path")
}
return path[n:]
}
The following line may be rewritten:
CacheDuration: time.Duration(1) * time.Hour,
into
CacheDuration: time.Hour,
Panic may occur on the following line due to the same reason mentioned above:
fileName := urlPart[4]
I'd recommend using the following code instead:
fileName := ctx.URI().LastPathSegment()
The following expression may be shortened:
ctx.IsGet() == true || ctx.IsHead() == true
into
ctx.IsGet() || ctx.IsHead()
As for Content-Disposition: attachment response header, it looks like it would be great adding the corresponding config parameter to FS (for example, FS.ServeAttachments), so fasthttp could automatically adding the header when the parameter is set.
Just noticed that I recommended non-equivalent path rewriter function. Your function strips the trailing part of the path. So the code must be:
rw := func(ctx *fasthttp.RequestCtx) []byte {
// fasthttp guarantees that the path starts with '/'
path := ctx.Path()[1:]
n := bytes.IndexByte(path, '/')
if n < 0 {
return []byte("/invalid-path")
}
nn := bytes.LastIndexByte(path, '/')
return path[n:nn]
}
I didn't test it, so it may contain bugs. But I hope you caught the idea.
@valyala thanks a lot for the explanation in details
Most helpful comment
Short story: fasthttp works faster than nginx on small files (up to 8Kb on my laptop). When serving big files, its' performance it about 80% comparing to nginx performance, because go's sendfile code path isn't optimized yet comparing to nginx.
Below are random remarks regarding the provided code:
The following line is not necessary, since Go automatically sets GOMAXPROCS to runtime.NumCPU() starting from go 1.5:
The following line may panic, since it assumes that the requested path contains at least four parts delimited by '/' . This is not true for arbitrary user input:
This line also makes at least two memory allocations - the first one for string concatenation and the second one for
stringto[]byteconversion. This may degrade performance a bit.An additional memory allocation is made on the line for
[]stringslice returned fromstrings.Split:I'd recommend rewriting
rwin the following way:The following line may be rewritten:
into
Panic may occur on the following line due to the same reason mentioned above:
I'd recommend using the following code instead:
The following expression may be shortened:
into
As for
Content-Disposition: attachmentresponse header, it looks like it would be great adding the corresponding config parameter toFS(for example,FS.ServeAttachments), so fasthttp could automatically adding the header when the parameter is set.