Tag

json api server

2 articles
Trains

How to build a Rails API server: Optimizing the framework

I have been developing Rails JSON API applications for quite some time now, and I'd like to share a few of my setups and discuss why I do things this way. I'm starting today a series of articles that will cover up pretty much the steps I take every time I bootstrap a new Rails JSON API application.

One of the first things I do is to ensure I'm optimizing Rails for speed. I basically optimize the framework itself, prior coding any specific application logic.

You may have heard before that "Premature optimization is the root of all evil". However, "Premature optimization is a phrase used to describe a situation where a programmer lets performance considerations affect the design of a piece of code", which "can result in a design that is not as clean as it could have been or code that is incorrect, because the code is complicated by the optimization and the programmer is distracted by optimizing" (source: WikiPedia). This is not what we're doing here: we're just going to apply a few changes to Rails, and then basically forget about those and start coding in a framework that is optimized to serve our API.

Many of Rails functionalities are simply not needed when building an API server, and by stripping down Rails to a bare minimum we can actually achieve pretty significant performance increases.

Greenfield Ruby On Rails

Let's first see what an empty project can achieve. I'm currently using Ruby 2.2.2 and Rails 4.2.1. Let's create a new Rails application:

rails new api_greenfield -T

Let's add a production server. For the scope of this post, it's not really important what we use, as long as it's a server that we can use in production. We are going to benchmark the results we get after applying our changes to Rails, so the absolute values resulting from our benchmarks are not as important as the relative improvements that we see in speed.

We're going to use Puma, as it is now the recommended Ruby webserver by Heroku (and as I host most of my applications there, using it has become my default choice). Add it to the project Gemfile:

Gemfile
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'sqlite3'

gem 'puma'

Then bundle install. Create a Puma configuration file config/puma.rb  and set the following basic params:

RUBY
workers 4
threads_count = 1
threads threads_count, threads_count

preload_app!

rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RAILS_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end

We now need to set up a simple response page that we will hit with our benchmarks. We're going to create a controller and an action that responds with a JSON body to the entry point /benchmarks/simple. To do so, let's create benchmarks_controller.rb:

RUBY
class BenchmarksController < ApplicationController

  def simple
    # example from http://json.org/example
    json = {
      glossary: {
        title: "example glossary",
        gloss_div: {
          title: "S",
          gloss_list: {
            gloss_entry: {
              id: "SGML",
              sort_as: "SGML",
              gloss_term: "Standard Generalized Markup Language",
              acronym: "SGML",
              abbrev: "ISO 8879:1986",
              gloss_def: {
                para: "A meta-markup language, used to create markup languages such as DocBook.",
                gloss_see_also: ["GML", "XML"]
              },
              gloss_see: "markup"
            }
          }
        }
      }
    }

    render json: json
  end
end

Set the routes for this controller:

RUBY
Rails.application.routes.draw do
  resources :benchmarks, only: :none do
    collection do
      get :simple
    end
  end
end

Start Puma in production:

BASH
RAILS_ENV=production bundle exec puma -C config/puma.rb

Verify that Rails responds with our JSON body at the chosen entry point:

$ curl -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple
{"glossary":{"title":"example glossary","gloss_div":{"title":"S","gloss_list":{"gloss_entry":{"id":"SGML","sort_as":"SGML","gloss_term":"Standard Generalized Markup Language","acronym":"SGML","abbrev":"ISO 8879:1986","gloss_def":{"para":"A meta-markup language, used to create markup languages such as DocBook.","gloss_see_also":["GML","XML"]},"gloss_see":"markup"}}}}}

The server is up and ready. We can now benchmark our greenfield Rails application running with Puma. We will use the basic Apache Benchmark tool to do so.

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple
This is ApacheBench, Version 2.3 <$Revision: 1604373
gt; Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 3000 Document Path: /benchmarks/simple Document Length: 369 bytes Concurrency Level: 5 Time taken for tests: 4.676 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 6990000 bytes HTML transferred: 3690000 bytes Requests per second: 2138.53 [#/sec] (mean) Time per request: 2.338 [ms] (mean) Time per request: 0.468 [ms] (mean, across all concurrent requests) Transfer rate: 1459.79 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 2 1.0 2 27 Waiting: 1 2 1.0 2 27 Total: 1 2 1.0 2 27 Percentage of the requests served within a certain time (ms) 50% 2 66% 2 75% 3 80% 3 90% 3 95% 4 98% 4 99% 5 100% 27 (longest request)

This is actually not bad at all! A greenfield Rails project is able to sustain 2,138 req/sec. Obviously, this is without any application logic, nor database calls, but it is still a good starting point.

The Rails API gem

The Rails API gem is "a subset of a normal Rails application, created for applications that don't require all functionality that a complete Rails application provides. It is a bit more lightweight, and consequently a bit faster than a normal Rails application. The main example for its usage is in API applications only, where you usually don't need the entire Rails middleware stack nor template generation".  Note that Rails API will be part of Rails 5, but for now we still have to include the gem:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'rails-api'
gem 'sqlite3'

gem 'puma'

Don't forget to bundle install. Then, change our benchmarks_controller.rb to inherit from the Rails::API Action Controller:

RUBY
class BenchmarksController < ActionController::API

Also, comment out in application_controller.rb :

RUBY
# protect_from_forgery with: :exception

Let's try a new benchmark (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   4.220 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      6990000 bytes
HTML transferred:       3690000 bytes
Requests per second:    2369.39 [#/sec] (mean)
Time per request:       2.110 [ms] (mean)
Time per request:       0.422 [ms] (mean, across all concurrent requests)
Transfer rate:          1617.39 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     1    2   0.9      2      27
Waiting:        1    2   0.9      2      27
Total:          1    2   0.9      2      27

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      3
  90%      3
  95%      3
  98%      4
  99%      4
 100%     27 (longest request)

We now see a response rate of 2,369 req/sec, which is an increase in performance of ~11% over greenfield Rails. This is a modest improvement, but an improvement nonetheless.

OJ

Rails' default JSON serializer isn't the fastest out there, so let's swap it for Oj:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
gem 'rails-api'
gem 'sqlite3'

gem 'puma'

gem 'oj'
gem 'oj_mimic_json'

Let's run the benchmark with Oj (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   4.040 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      6990000 bytes
HTML transferred:       3690000 bytes
Requests per second:    2475.34 [#/sec] (mean)
Time per request:       2.020 [ms] (mean)
Time per request:       0.404 [ms] (mean, across all concurrent requests)
Transfer rate:          1689.71 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0      27
Processing:     1    2   0.8      2      29
Waiting:        1    2   0.8      1      29
Total:          1    2   0.9      2      29
WARNING: The median and mean for the waiting time are not within a normal deviation
        These results are probably not that reliable.

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      3
  90%      3
  95%      3
  98%      3
  99%      4
 100%     29 (longest request)

We can see a small improvement here, which is practically irrelevant (~4%) as we hit 2,475 req/sec. The switch to Oj is going to be more relevant the bigger the JSON objects to serialize are, but at this stage it doesn't hurt to keep Oj in here.

ActionController::Metal

It is now time to give the final boost, by:

  • Removing unnecessary railties.
  • Using Rails' ActionController::Metal instead of the base controllers that our BenchmarkController has inherited from until now.

First, remove unnecessary imports from application.rb (your mileage may vary - this is my standard setup and I've rarely needed anything else):

RUBY
# require "active_model/railtie"
# require "active_job/railtie"
require "active_record/railtie"
# require "action_controller/railtie"
require "action_mailer/railtie"
# require "action_view/railtie"
# require "sprockets/railtie"

Second (and this is what is really going to make a difference), we're going to create a new controller that all of our API controllers are going to inherit from. Let's create our base api_controller.rb :

RUBY
class ApiController < ActionController::Metal
  abstract!

  include AbstractController::Callbacks
  include ActionController::RackDelegation
  include ActionController::StrongParameters

  private

  def render(options={})
    self.status = options[:status] || 200
    self.content_type = 'application/json'
    body = Oj.dump(options[:json], mode: :compat)
    self.headers['Content-Length'] = body.bytesize.to_s
    self.response_body = body
  end

  ActiveSupport.run_load_hooks(:action_controller, self)
end

As you can see, in this controller we define our custom render method. By default, I've already included the three modules that I basically use everywhere:

  • AbstractController::Callbacks which allows you to set callbacks such as before_action  in your controllers.
  • ActionController::RackDelegation which is needed to set the response_body  (called in the render  method).
  • ActionController::StrongParameters which allows you to use Strong Params in your controllers.

Other modules that you might want to include here are, for instance:

  • ActionController::HttpAuthentication::Token::ControllerMethods to use the authenticate_with_http_token  helper method if you are going to use token authentication in your API.
  • ActionController::HttpAuthentication::Basic::ControllerMethods to use the authenticate_with_http_basic  helper method if you are going to use basic authentication in your API.

Now for our benchmarks, let's ensure that benchmarks_controller.rb  inherits from our newly created controller:

RUBY
class BenchmarksController < ApiController

Here are the results of the benchmark that includes all of above changes (portions omitted):

$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple

[...]

Concurrency Level:      5
Time taken for tests:   2.377 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      7200000 bytes
HTML transferred:       3690000 bytes
Requests per second:    4206.19 [#/sec] (mean)
Time per request:       1.189 [ms] (mean)
Time per request:       0.238 [ms] (mean, across all concurrent requests)
Transfer rate:          2957.48 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     1    1   0.4      1       5
Waiting:        0    1   0.4      1       5
Total:          1    1   0.4      1       5

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      2
  95%      2
  98%      2
  99%      2
 100%      5 (longest request)

This time the impact is notable, as we hit 4,206 req/sec.

Final Touch

With our latest ApiController, we are not using the controller that the Rails API gem exposes to us. Therefore, let's remove the gem:

RUBY
source 'https://rubygems.org'
ruby '2.2.2'

gem 'rails', '4.2.1'
# gem 'rails-api'
gem 'sqlite3'

gem 'puma'

gem 'oj'
gem 'oj_mimic_json'

However, the Rails API gem did other interesting things under the hood, such as disabling some unnecessary Rails middleware. Since we removed it, we now need to do so ourselves. Add to application.rb:

RUBY
module ApiGreenfield
  class Application < Rails::Application

    [...]

    # remove unnecessary middleware
    config.middleware.delete Rack::Sendfile
    config.middleware.delete Rack::MethodOverride
    config.middleware.delete ActionDispatch::Cookies
    config.middleware.delete ActionDispatch::Session::CookieStore
    config.middleware.delete ActionDispatch::Flash
  end
end

Running the benchmark returns the previous results, so we can safely say we don't need the Rails API gem anymore.

Conclusions

We have started with a greenfield Rails project, and have gradually applied changes to improve the speed performance of a simple benchmarked application:

[table]
Version,Req/sec,Increase
Greenfield Rails,"2,138",-
+ Rails API Gem,"2,369",+11%
+ Rails API Gem + Oj,"2,475",+15%
+ Oj + ActionController::Metal + Custom middleware,"4,206",+97%
[/table]

Overall, we experienced an increase from 2,138 to 4,206 req/sec, which is doubling the initial performance of a greenfield Rails application.

For additional boosts, you may consider caching techniques (such as partial JSON caching), which are application dependent and are therefore out of scope here.

Happy API'ing!

Continue Reading…
gin

GIN: a JSON-API framework in Lua

GIN is a fast, low-latency, low-memory footprint, web JSON-API framework with Test Driven Development helpers and patterns. It is a framework that I open sourced some time ago, as an experiment. 

It all started when I heard so many good things about Lua that I wanted to see it in action and find a project where I could unleash its power. Being an API fan, it came natural for me to build a JSON-API server framework. And GIN was born.

GIN is currently in its early stage, but it already enables fast development, TDD and ease of maintenance. It is helpful when you need an extra-boost in performance and scalability, since it is entirely written in Lua and it runs embedded in a packaged version of nginx called OpenResty. For those not familiar with Lua, don't let that scare you away: Lua is really easy to use, very fast and simple to get started with.

Controllers

The syntax of a controller is extremely simple. For instance, a simple controller that returns an application's version information looks like:

LUA
local InfoController = {}

function InfoController:root()
    return 200, { id = 'my_api', description = 'An API server powered by GIN.' }
end

return InfoController

The return statement specifies:

  • The HTTP code 200
  • The body of the response, which gets encoded by GIN into JSON as:
JAVASCRIPT
{
  "id": "my_api",
  "description": "An API server powered by GIN."
}

Most of standard JSON-API paradigms are already embedded in GIN.

Versioning

For instance, Versioning is extremely simple. Major versioning is baked into GIN. Based on directory naming conventions, the controllers that correspond to the versioning specified in the header will be called by GIN automatically. Gin uses the HTTP header Accept to version your API. The header has the format:

application/vnd.{application-name}.v{version}+json

So for instance, if your application name (as defined in the file ./config/application.lua) is demo, a client trying to access the version 1 of your API will have to send the header:

Accept: application/vnd.demo.v1+json

Gin only accepts requests that provide JSON in the body, hence the +json portion of the Accept header.

Routing

Routing is very easy, too. For instance, this is an example of a routing file that handles multiple versions:

LUA
local routes = require 'gin.core.routes'

-- define version 1
local v1 = routes.version(1)

-- define routes
v1:GET("/users", { controller = "users", action = "index" })
v1:POST("/users", { controller = "users", action = "create" })

-- define version 2
local v2 = routes.version(2)

-- define routes
v2:GET("/users", { controller = "users", action = "index" })
v2:POST("/users", { controller = "users", action = "create" })
v2:GET("/users/:id", { controller = "users", action = "show" })

return routes

Errors

Moreover, Errors are defined globally at application level. This allows to keep track of error numbering and descriptions in a single file. For example, an error file is defined as:

LUA
local Errors = {
    [1000] = { status = 400, message = "Invalid request.", headers = { ["X-Header"] = "header" } },
}
return Errors

The global variable Errors contains the entries for all possible errors of your application. Every item in this table defines an error, where:

  • The 1000 key is the error number.
  • status (required): is the HTTP status code.
  • message (required): is the error description.
  • headers (optional): are the additional headers to be returned in the response.

When this is defined, then raising these errors from a controller is as simple as calling:

LUA
self:raise_error(1000)

As per this example, this will return a HTTP status 400, with the specified headers and the JSON body:

JAVASCRIPT
{
  "code": 1000,
  "message": "Invalid request."
}

Testing

However, this wouldn't be a complete framework if it didn't provide an easy way to test your application. The main testing framework embedded in GIN is Busted, which has an RSpec-like syntax. If you ever used Jasmine you'll feel right at home. In addition, Gin provides test helpers for you to use.

Controller Tests in Gin are actually integration tests. An instance of OpenResty nginx will be started and a real request will be issued to a test server. The main Gin test helper you'll need to test Controllers is the method hit, that will perform the integration test for you.

Here's an example of a controller test (file located in ./spec/controllers/1/info_controller_spec.lua), that is a valid test for the InfoController shown here above:

LUA
require 'spec.spec_helper'

describe("InfoController", function()
    describe("#root", function()
        it("responds with application info", function()
            local response = hit({
                method = 'GET',
                path = "/"
            })

            assert.are.same(200, response.status)
            assert.are.same({ id = 'my_api', description = 'An API server powered by GIN.' }, response.body)
        end)
    end)
end)

Others

GIN also has models and support for multiple databases, and includes a migration engine. Furthermore, it is packed with a lot of other features, such as:

  • An API Console (which allows you to try out your API from a web interface).
  • Support for multiple environments.
  • A client console.

Alright, I want to see it!

GIN main website is gin.io, where you'll find full documentation, and even a full tutorial on how to Test Drive your Development with GIN. Full code is available on Github.

Would this be worthy more than a simple experiment?

Continue Reading…