The upper limit

Ruby on Rails is slow. When the full-stack framework offers you an all-in-one solution the weaknesses exist, especially the incoming payloads that are continuously slapping you in the face. On most occasions, we just need to optimize our database or move the storage processes into queue system (for example, sidekiq), until we have tried everything to boost the performance. In the end we will reach the upper limit of rails.

Break through the bounds of “Rails”

Rails is a rack-based framework, an over bloated rack-based framework. @Eugene Melnikov offers a comparison sheet about the performance of Ruby frameworks. According to this sheet, we can find out that the speed of rack is almost 28 times that of Rails.

enter image description here

So what causes the slowness of Rails?
If you input the
$ rails middleware
You can get a middlewares list as follows:

use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
...

(Full list https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack)

How middleware works

Every time a rack receives a request (we call it Common Gateway Interface/CGI), Rack will convert it into an env object, and pass it to the Rails application (Your Rails application). The env object is a hash, which contains the URL, the body the rack version etc., it includes all the information which can be used to generate the route and parameters, but this primitive env object just offers the resource of generating the request in controller.

However, we don’t have the request, action controller yet. What I just mentioned causes the slowness of Rails, because the process of converting an env object consumes a lot of computer resources. The solution is obvious, we just need to skip the unnecessary middlewares and generate the response when we have already collected enough variables which can meet our logic.

A Simple Middleware

Rails offers us an easy way to insert our own middleware into any location of the rails middlewares(config/application.rb)
config.middleware.insert_after ActionDispatch::Executor, Our::MiddleWare
Middleware could be a class containing two methods:

module Our
  class MiddleWare
    def initialize(app)
      @app = app
    end
    def call(env)
       puts env
       if env["REQUEST_URI"].include?("middleware")
        [
          200,
          {"Content-Type" => "application/text"},
          ["THIS IS FROM MIDDLEWARE"]
        ]
      else
        @app.call(env)
      end
    end
  end
end

The code is simple if the URL variable has keyword “middleware”
Our middleware will directly return a response, otherwise our middleware will ignore the request and pass the env object to the next middleware, which is the normal Rails way.

Nevertheless

We also can access the ActiveRecord (Our models) in the middlewares, but it definitely is not a good idea, because we sacrificed heaps of Rails features to get about 50% speed up. If we access the database, it will lead to more than 80% speed drop.

Maybe some people will argue that if we cannot access the storage, the middleware cannot handle any logic. The answer is yes and no. We can store and query the data from Redis, especially since the writing speed of Redis can reach 1 million req/s.

A classic method directly inserts the data payload into Redis and use the sidekiq to fetch and process from Redis. In terms of normal requests, middleware is not a good practice, we just need to move our highest work loading into middleware for dealing with the 5% most simple but costly requests. Never ever try to move all your logics into middleware or you can choose Sinatra as your web framework.

Conclusion & Prospect

Understanding how Rails works can help developers to deal with most performance problems. Before we decide to move some logic into middleware, we still need to double check if the database, SQL queries, and caches are well optimized. Squeezing the last drop of Rails performance, it is endless.

Thanks for reading. In my next post I will introduce how to insert the request payloads into Redis from Nginx (Yep, from Nginx ^_^) and allow the sidekiq reading the payload from Redis because Rack is slow.