Fasthttp: FileServer speed

Created on 9 Feb 2016  路  3Comments  路  Source: valyala/fasthttp

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)
    }

}

question

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:

    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.

All 3 comments

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bullardla picture bullardla  路  5Comments

imgurbot12 picture imgurbot12  路  5Comments

AnikHasibul picture AnikHasibul  路  3Comments

unisqu picture unisqu  路  4Comments

kirillDanshin picture kirillDanshin  路  5Comments