gofiber / fiber

⚡️ Express inspired web framework written in Go
https://gofiber.io
MIT License
34.04k stars 1.67k forks source link

Why is fiber slower than gin when include validation #1734

Closed red010182 closed 2 years ago

red010182 commented 2 years ago

I am making a simple benchmark for some performance-oriented web frameworks like gin, fiber, actix, axum. fiber is undoubtedly an awesome framework which is probably the fatest in golang and is super fast before I add query parse and validation code.

However, after I add query parse and validation code, fiber is both slower and consumes larger memory than gin. Here's the numbers:

Before: fiber: 30k req/s, 25 MB (mem) gin: 24k req/s, 20 MB (mem)

After: fiber: 21k req/s, 60 MB (mem) gin: 24k req/s, 20 MB (mem)

OSX MBP, 2.3 GHz Dual-Core Intel Core i5, 16 GB Memory

ps. token is sent as query param for test convenience.

Maybe the question should be what is the right way to validate params in fiber without dropping lots of performance?

// fiber main.go
package main

import (
    "fmt"

    "github.com/go-playground/validator"
    "github.com/golang-jwt/jwt"

    fiber "github.com/gofiber/fiber/v2"
)

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}
type Hello struct {
    Result float64   `json:"result"`
    Name   string    `json:"name"`
    Users  [100]User `json:"users"`
}
type HelloQuery struct {
    Token string `query:"token", validate:"required"`
}

func main() {
    var key []byte = []byte("some-secret")
    app := fiber.New()

    app.Get("/hello/:name", func(c *fiber.Ctx) error {
        var name string = c.Params("name")

        query := HelloQuery{}

                 //====== query parse and validation start ====
        if err := c.QueryParser(&query); err != nil {
            return fiber.ErrBadRequest
        }
        validate := validator.New()
        if err := validate.Struct(query); err != nil {
            return fiber.ErrBadRequest
        }
                //=============== end ===============

        var nameFromToken string
        token, err := jwt.Parse(query.Token, func(token *jwt.Token) (interface{}, error) {
            return key, nil
        })

        if err != nil {
            return fiber.ErrBadRequest
        }

        if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
            nameFromToken = fmt.Sprintf("%v", claims["name"])
        }

        var users [100]User
        for i := 0; i < len(users); i++ {
            users[i] = User{
                Name: "Go Fiber",
                Id:   i + 1,
            }
        }

        return c.JSON(Hello{
            Name:   fmt.Sprintf("Hi, %s, you're %s actually.", name, nameFromToken),
            Result: 200.0 / 3.0,
            Users:  users,
        })
    })

    app.Listen(":3005")
}

test command

http://localhost:3005/hello/Bill_Gates?token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiTm9ib2R5In0.PlSVSvvWrbkpwGL8uy89kEP5s4pTV6dWQNYyHXQWZHc

// gin main.go
package main

import (
    "fmt"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt"
)

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}
type Hello struct {
    Result float64   `json:"result"`
    Name   string    `json:"name"`
    Users  [100]User `json:"users"`
}
type HelloQuery struct {
    Token string `form:"token" binding:"required"`
}

func main() {
    var key []byte = []byte("some-secret")

    gin.SetMode(gin.ReleaseMode)
    r := gin.New()

    r.GET("/hello/:name", func(c *gin.Context) {

                 //============== query parse and validation start =============
        var query HelloQuery
        if err := c.Bind(&query); err != nil {
            c.AbortWithStatusJSON(400, gin.H{
                "message": err,
            })
            return
        }
                //=================== end =========================

                 var nameFromToken string
        token, _ := jwt.Parse(query.Token, func(token *jwt.Token) (interface{}, error) {
            return key, nil
        })

        if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
            nameFromToken = fmt.Sprintf("%v", claims["name"])
        }

        var users [100]User
        for i := 0; i < len(users); i++ {
            users[i] = User{
                Name: "Gin",
                Id:   i + 1,
            }
        }

        c.JSON(200, Hello{
            Name:   fmt.Sprintf("Hi, %s, you're %s actually.", c.Param("name"), nameFromToken),
            Result: 200.0 / 3.0,
            Users:  users,
        })
    })
    r.Run("0.0.0.0:3002")
}

test command

http://localhost:3002/hello/Bill_Gates?token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiTm9ib2R5In0.PlSVSvvWrbkpwGL8uy89kEP5s4pTV6dWQNYyHXQWZHc

balcieren commented 2 years ago

@red010182 may you try with github.com/go-playground/validator/v10 for fiber.

balcieren commented 2 years ago

@red010182 versions are low. I think you should try with lastest versions.

red010182 commented 2 years ago

@balcieren Still slow(20k req/s) and memory almost doubled. But, if I use a shared validator, it speeds up to 29k req/s and memory is reduced to 35MB. What I mean is all requests share the same validator. Like this:

import (
    "github.com/go-playground/validator/v10"
        ...
)

...

func main() {
    var key []byte = []byte("some-secret")
    app := fiber.New()

    validate := validator.New() // <--- init here

    app.Get("/hello/:name", func(c *fiber.Ctx) error {
        ...

        if err := validate.Struct(query); err != nil {
            return fiber.ErrBadRequest
        }

...

It turns out to be that the alloc/dealloc of validator seems very expensive. However, I'm not sure if it's a good idea to use a single validator for all requests and all different struct types.

balcieren commented 2 years ago

@balcieren Still slow(20k req/s) and memory almost doubled. But, if I use a shared validator, it speeds up to 29k req/s and memory is reduced to 35MB. What I mean is all requests share the same validator. Like this:

import (
  "github.com/go-playground/validator/v10"
        ...
)

...

func main() {
  var key []byte = []byte("some-secret")
  app := fiber.New()

  validate := validator.New() // <--- init here

  app.Get("/hello/:name", func(c *fiber.Ctx) error {
      ...

      if err := validate.Struct(query); err != nil {
          return fiber.ErrBadRequest
      }

...

It turns out to be that the alloc/dealloc of validator seems very expensive. However, I'm not sure if it's a good idea to use a single validator for all requests and all different struct types.

Validator should be created once.

mikeychowy commented 2 years ago

Validator should be created once.

Correct, why would you want different instances of validator for each request? Of course if you need to make an instance of a heavy object each request it's going to be slow and resource heavy.

Gin also instantiate it only once, hell, take any web framework of any language, like spring boot, and they will instantiate it only once

balcieren commented 2 years ago

Validator should be created once.

Correct, why would you want different instances of validator for each request? Of course if you need to make an instance of a heavy object each request it's going to be slow and resource heavy.

Gin also instantiate it only once, hell, take any web framework of any language, like spring boot, and they will instantiate it only once

Yes that's right, I agree you.

balcieren commented 2 years ago

Maybe can be developed custom validator for fiber like Gin's custom validator. It can be accessed from fiber.Ctx like c.Validate().

mikeychowy commented 2 years ago

Maybe can be developed custom validator for fiber like Gin's custom validator. It can be accessed from fiber.Ctx like c.Validate().

If it's plug and play like how gin's works so i can easily bring my own implementations then I'm 100% on board

red010182 commented 2 years ago

Validator should be created once.

Correct, why would you want different instances of validator for each request?

Because I was guided by the official tutorial https://docs.gofiber.io/guide/validation, in which a new validator is created in each request. So I thought it was a "light-weight" validator before.

Gin also instantiate it only once, hell, take any web framework of any language, like spring boot, and they will instantiate it only once

Thanks a lot. I didn't know Gin instantiate it only once.

mikeychowy commented 2 years ago

Hmmm, maybe should have a disclaimer that those code examples are for demonstration purposes only.

If you want better examples of real usages, maybe go through the boilerplates listed here

ReneWerner87 commented 2 years ago

my code:

package main

import (
    "log"

    "github.com/go-playground/validator/v10"
    "github.com/gofiber/fiber/v2"
)

type Job struct{
    Type          string `validate:"required,min=3,max=32"`
    Salary        int    `validate:"required,number"`
}

type User struct{
    Name          string  `validate:"required,min=3,max=32"`
    // use `*bool` here otherwise the validation will fail for `false` values
    // Ref: https://github.com/go-playground/validator/issues/319#issuecomment-339222389
    IsActive      *bool   `validate:"required"`
    Email         string  `validate:"required,email,min=6,max=32"`
    Job           Job     `validate:"dive"`
}

type ErrorResponse struct {
    FailedField string
    Tag         string
    Value       string
}

var validate = validator.New() // <-- validator initialized once
func ValidateStruct(user User) []*ErrorResponse {
    var errors []*ErrorResponse
    //validate := validator.New() // <-- validator initialized every request
    err := validate.Struct(user)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            var element ErrorResponse
            element.FailedField = err.StructNamespace()
            element.Tag = err.Tag()
            element.Value = err.Param()
            errors = append(errors, &element)
        }
    }
    return errors
}

func AddUser(c *fiber.Ctx) error {
    //Connect to database

    user := new(User)

    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "message": err.Error(),
        })

    }

    errors := ValidateStruct(*user)
    if errors != nil {
        return c.Status(fiber.StatusBadRequest).JSON(errors)

    }

    //Do something else here

    //Return user
    return c.JSON(user)
}

func main() {
    app := fiber.New()

    //app.Post("/", func(ctx *fiber.Ctx) error { // <-- request without any additional process
    //   return ctx.SendString("OK")
    //})
    app.Post("/", AddUser)

    log.Fatalln(app.Listen(":8080"))
}

tool + request code:

bombardier -c 10 -d 10s "http://localhost:8080/" -m POST --body='{"name": "test","isActive": true,"email": "test@test.com","job": { "type": "tester", "salary": 30000 }}' --header="Content-Type: application/json"

case 1 - "request without any additional process" image

case 2 - "validator initialized every request" image

case 3 - "validator initialized once" image

in gin: https://github.com/gin-gonic/gin/blob/57ede9c95abb4bc39f471b101181eb06938b5d7f/binding/default_validator.go#L92-L97

my change in the example: https://github.com/gofiber/docs/commit/98f9d00d2e2e6dbe8dd61af2c2e03d44e5fccf37 https://docs.gofiber.io/guide/validation

ReneWerner87 commented 2 years ago

@red010182 thank you for the report and to all involved for the hints

red010182 commented 2 years ago

@ReneWerner87 It's my honor to contribute a little bit to the community.

balcieren commented 2 years ago

Maybe can be developed custom validator for fiber like Gin's custom validator. It can be accessed from fiber.Ctx like c.Validate().

If it's plug and play like how gin's works so i can easily bring my own implementations then I'm 100% on board

https://github.com/gofiber/fiber/pull/1766