Benchmarking Go, Gin, and Heroku

Semantics, semantics...

I'm not sure if it is conditioning, or aesthetics, but I enjoy C-style syntax, and I feel at home in languages that follow similar semantics. As a full stack web developer, I am constantly looking for new techniques to build services and applications at scale. Surprisingly enough, I'm just now taking a look at Google's Go.

Having recently completed the design and implementation of an online game system to support lottery games built in Unity 3D (and targeting WebGL), I am interested in using hindsight to assess how I might have engineered various pieces a bit differently. In the regulated gaming industry, the Mersenne Twister is often used as a software pseudo-random number generator (PRNG). Since RNGs form a crucial component of various regulated gaming systems, it seems creating a durable RNG service that combines speed and stability is a high priority.

(In the future, we may explore porting this example to Erlang, Elixir, and other slightly esoteric languages.)

Do one thing well

As a huge fan of "The UNIX Philosophy," it always occurs to me that a micro-service should do one task, and do it rather well.

The code for this demo service is on GitHub and released under the MIT License.

Here is the main code listing (with some thanks to @CasualSuperman on GitHub for the Mersenne Twister in Go):

The code

package main

import (  
    "log"
    "net/http"
    "os"
    "sync"
    "math/rand"
    "github.com/gin-gonic/gin"
)

// https://github.com/CasualSuperman/Mersenne-Twister
type Generator struct{  
    state [624]uint32
    index uint32
    generating sync.Mutex
}

func Init(a uint32) *Generator{  
    var t Generator
    t.init(a)
    return &t
}

func (g *Generator) init(a uint32){  
    g.generating.Lock()
    g.state[0] = a
    var i uint32 = 0
    for i=1; i<624; i++{
        g.state[i] = uint32(0x6c078965 * (g.state[i-1] ^
        g.state[i-1]>>30) + i)
    }
    g.generating.Unlock()
}

func (g *Generator) Next() uint32{  
    g.generating.Lock()
    if g.index == 0{
        g.generateNumbers()
    }
    y := g.state[g.index]
    g.index++
    g.index %= 624
    g.generating.Unlock()
    y ^= y>>11
    y ^= y<<7 & 0x9d2c5680
    y ^= y<<15 & 0xefc60000
    y ^= y>>18
    return y
}

func (g *Generator) generateNumbers(){  
    var i uint32
    for i=0; i<624; i++{
        y := g.state[i]>>31 + g.state[(i+1)%624] & 0x7fffffff
        g.state[i] = g.state[(i+397)%624] ^ y>>1
        if y%2 == 1{
            g.state[i] ^= 0x9908b0df
        }
    }
}

func main() {  
    port := os.Getenv("PORT")

    rng := Init(rand.Uint32())

    if port == "" {
        log.Println("$PORT must be set.  Setting to 8080.")
        port = "8080"
    }

    router := gin.New()
    router.Use(gin.Logger())
    router.Static("/static", "./static")

    router.GET("/loaderio-fad37ceadee65cb4bdf707027748bd42", func(c *gin.Context) {
        c.Writer.Write([]byte("loaderio-fad37ceadee65cb4bdf707027748bd42")) // Passes Loader.io verification :)
    })

    router.GET("/", func(c *gin.Context) {
        //c.HTML(http.StatusOK, "index.tmpl.html", nil)
        c.JSON(http.StatusOK, gin.H{
            "rng": rng.Next(),
        })
    })

    router.Run(":" + port)
}

Dyno Might

For this project, I decided to deploy our Go micro service, written using the Gin framework, to a free Heroku instance. To be honest, I wasn't very surprised at the results (well, after a bit of trouble getting the slug to compile and bind to $PORT).

My reasoning is that squeezing some great performance out of a single free-tier Heroku dyno is an interesting exercise in tasting the potential of Go micro services. Also, I didn't want to provision my own dedicated application server, nor add this micro service to an existing application server.

(In the future, it wil be interesting to see if I can get this micro service running on an Azure Service Fabric cluster; both on localhost, and on Azure.)

Measure twice, then cut

Here are some results from Loader.io. Here, I also used their free plan; particularly since their free plan is adequate to test my single end-point.

Test 1

View on loader.io

Test 2

View on loader.io

Test 3

View on loader.io

Test 4

View on loader.io

Test 5

View on loader.io

Over 300,000

Test 5 was the most successful. In one minute, we were able to serve up 374,307 pseudo-random numbers with each request returning a 200 OK.

374,307 successful requests/responses. Not a single 400/500 error. On a single free dyno.

And that with an average response time of 190 ms.